Compare commits

...

169 Commits

Author SHA1 Message Date
agnostic-apollo
43317b78c9 Release: v0.118.1
The `versionCode` has been bumped to `1000` so that users who have installed from F-Droid or GitHub should not have the app attempted to be updated by Google PlayStore and failing and also shown in PlayStore app updates list due to non-collaborative `v0.120` app release on PlayStore that set the `versionCode` higher than the latest F-Droid or GitHub `118` release. Unlike F-Droid, PlayStore does not check for difference in app APK signature before attempting to download and then failing to install due to signature mismatch.

- https://github.com/termux/termux-app/discussions/4000
- https://github.com/termux/termux-app/issues/4012
2024-06-18 04:10:09 +05:00
agnostic-apollo
2a008d836e Changed: Update support and donate users to termux.dev domain 2024-06-18 04:00:51 +05:00
agnostic-apollo
daa7ca4d43 Changed: Bump apt-android-7 bootstraps to 2024.06.17-r1 2024-06-18 03:55:30 +05:00
agnostic-apollo
2c82a5581f Added: Add support for Termux bootstrap second stage by running termux-bootstrap-second-stage.sh
- 7827140577
- 7827140577/scripts/bootstrap/termux-bootstrap-second-stage.sh
2024-06-18 03:55:30 +05:00
agnostic-apollo
708281cea2 Changed: Bump actions/checkout and actions/upload-artifact to v4 2024-06-18 03:36:37 +05:00
agnostic-apollo
eb0cb408a4 Changed: Use GitHub cli instead of hub for uploading GitHub release files as later has been removed from runner images
- https://github.com/actions/runner-images/issues/8362
2024-06-18 03:32:45 +05:00
agnostic-apollo
d11c95b996 Added: Add support for GitHub action builds for github-releases/** branches 2024-06-18 03:28:50 +05:00
agnostic-apollo
9735ae284d Added: Request SET_ALARM permission to allow broadcasting an intent to set an alarm or timer in an alarm clock app
- https://developer.android.com/reference/android/Manifest.permission#SET_ALARM
- https://developer.android.com/reference/android/provider/AlarmClock

Closes #3990
2024-06-18 03:28:17 +05:00
agnostic-apollo
0813e46330 Fixed: Limit max combining characters in TerminalRow to 15 characters to prevent buffer overflows
The exception below causing app crash happens because of malicious input where combining characters keep getting added to same column of the row and this increases the size of `mSpaceUsed` and `mText`, eventually causing a buffer overflow of `mSpaceUsed`, which is limited to max `32767` value as per java `short` limit, but the limit itself isn't the issue, but an endless number of combining characters being added. Check `MAX_COMBINING_CHARACTERS_PER_COLUMN` field javadocs for why the limit `15` was chosen.

```
curl -o matroska.js https://kimapr.net/lappy/matroska.js
cat matroska.js
```

The `charCount` below refers to value of `Character.charCount(codePoint)`, like before `oldCharactersUsedForColumn` is appended to `newCharactersUsedForColumn`.

```
TerminalRow: codePoint=112, mColumns=98, mText=637, columnToSet=18, mSpaceUsed=590, javaCharDifference=0, oldStartOfColumnIndex=510, oldCharactersUsedForColumn=1, newCharactersUsedForColumn=1, oldNextColumnIndex=511, newNextColumnIndex=511, charCount=1, oldCodePointDisplayWidth=1, newCodePointDisplayWidth=1
TerminalRow: codePoint=40, mColumns=98, mText=637, columnToSet=19, mSpaceUsed=590, javaCharDifference=0, oldStartOfColumnIndex=511, oldCharactersUsedForColumn=1, newCharactersUsedForColumn=1, oldNextColumnIndex=512, newNextColumnIndex=512, charCount=1, oldCodePointDisplayWidth=1, newCodePointDisplayWidth=1
TerminalRow: codePoint=40, mColumns=98, mText=637, columnToSet=20, mSpaceUsed=590, javaCharDifference=0, oldStartOfColumnIndex=512, oldCharactersUsedForColumn=1, newCharactersUsedForColumn=1, oldNextColumnIndex=513, newNextColumnIndex=513, charCount=1, oldCodePointDisplayWidth=1, newCodePointDisplayWidth=1
TerminalRow: codePoint=101, mColumns=98, mText=637, columnToSet=21, mSpaceUsed=590, javaCharDifference=0, oldStartOfColumnIndex=513, oldCharactersUsedForColumn=1, newCharactersUsedForColumn=1, oldNextColumnIndex=514, newNextColumnIndex=514, charCount=1, oldCodePointDisplayWidth=1, newCodePointDisplayWidth=1
TerminalRow: codePoint=917772, mColumns=98, mText=147, columnToSet=18, mSpaceUsed=98, javaCharDifference=2, oldStartOfColumnIndex=18, oldCharactersUsedForColumn=1, newCharactersUsedForColumn=3, oldNextColumnIndex=19, newNextColumnIndex=21, charCount=2, oldCodePointDisplayWidth=1, newCodePointDisplayWidth=0
I TerminalRow: codePoint=65024, mColumns=98, mText=147, columnToSet=18, mSpaceUsed=100, javaCharDifference=1, oldStartOfColumnIndex=18, oldCharactersUsedForColumn=3, newCharactersUsedForColumn=4, oldNextColumnIndex=21, newNextColumnIndex=22, charCount=1, oldCodePointDisplayWidth=1, newCodePointDisplayWidth=0
TerminalRow: codePoint=917772, mColumns=98, mText=147, columnToSet=18, mSpaceUsed=101, javaCharDifference=2, oldStartOfColumnIndex=18, oldCharactersUsedForColumn=4, newCharactersUsedForColumn=6, oldNextColumnIndex=22, newNextColumnIndex=24, charCount=2, oldCodePointDisplayWidth=1, newCodePointDisplayWidth=0
...
TerminalRow: codePoint=917959, mColumns=98, mText=32781, columnToSet=18, mSpaceUsed=32763, javaCharDifference=2, oldStartOfColumnIndex=18, oldCharactersUsedForColumn=32666, newCharactersUsedForColumn=32668, oldNextColumnIndex=32684, newNextColumnIndex=32686, charCount=2, oldCodePointDisplayWidth=1, newCodePointDisplayWidth=0
TerminalRow: codePoint=917939, mColumns=98, mText=32781, columnToSet=18, mSpaceUsed=32765, javaCharDifference=2, oldStartOfColumnIndex=18, oldCharactersUsedForColumn=32668, newCharactersUsedForColumn=32670, oldNextColumnIndex=32686, newNextColumnIndex=32688, charCount=2, oldCodePointDisplayWidth=1, newCodePointDisplayWidth=0
TerminalRow: codePoint=917961, mColumns=98, mText=32781, columnToSet=18, mSpaceUsed=32767, javaCharDifference=2, oldStartOfColumnIndex=18, oldCharactersUsedForColumn=32670, newCharactersUsedForColumn=32672, oldNextColumnIndex=32688, newNextColumnIndex=32690, charCount=2, oldCodePointDisplayWidth=1, newCodePointDisplayWidth=0
TerminalRow: codePoint=917804, mColumns=98, mText=32781, columnToSet=18, mSpaceUsed=-32767, javaCharDifference=2, oldStartOfColumnIndex=18, oldCharactersUsedForColumn=1, newCharactersUsedForColumn=3, oldNextColumnIndex=19, newNextColumnIndex=21, charCount=2, oldCodePointDisplayWidth=1, newCodePointDisplayWidth=0
```

```
java.lang.ArrayIndexOutOfBoundsException: src.length=32781 srcPos=19 dst.length=32781 dstPos=21 length=-32786
	at java.lang.System.arraycopy(System.java:469)
	at com.termux.terminal.TerminalRow.setChar(TerminalRow.java:196)
	at com.termux.terminal.TerminalBuffer.setChar(TerminalBuffer.java:455)
	at com.termux.terminal.TerminalEmulator.emitCodePoint(TerminalEmulator.java:2380)
	at com.termux.terminal.TerminalEmulator.processCodePoint(TerminalEmulator.java:624)
	at com.termux.terminal.TerminalEmulator.processByte(TerminalEmulator.java:520)
	at com.termux.terminal.TerminalEmulator.append(TerminalEmulator.java:487)
	at com.termux.terminal.TerminalSession$MainThreadHandler.handleMessage(TerminalSession.java:358)
	at android.os.Handler.dispatchMessage(Handler.java:106)
	at android.os.Looper.loop(Looper.java:223)
	at android.app.ActivityThread.main(ActivityThread.java:7664)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
```

See also following links for history of related changes to `TerminalRow` for combining characters. Note that jackpal terminal does not crash for above, which termux-app is based on, but changes were done by fornwall in initial commit of termux-app to change the behaviour, hence the crash, but he added the `FIXME: Put a limit of combining characters` comment as a note to solve the current issue in future, which is now.

- 9a47042620
- https://github.com/jackpal/Android-Terminal-Emulator/pull/338
- a18ee58f7a (diff-f84d215b18106c037e01986a3968fa54b74691174a78fcc99493f745d3805be5)

Closes #3839
2024-06-18 03:20:11 +05:00
Lucy Phipps
6ece249c03 WcWidth.c: fix 2nd typo 2024-06-18 03:20:11 +05:00
Lucy Phipps
fc8245bba3 WcWidth.c: fix typo 2024-06-18 03:20:11 +05:00
Lucy Phipps
63833d9c2d update WcWidth.java to Unicode 15.0.0 2024-06-18 03:20:11 +05:00
agnostic-apollo
c9e2a75e82 Fixed: Fix shared terminal transcript joining back lines
Regression of 370ac2bd caused in 5f71e3e7 by the (in)famous @trygveaa
2024-06-18 03:20:11 +05:00
agnostic-apollo
9433f10757 Fixed: Ensure CSI parameter value is not greater than 9999 as per vt510 2024-06-18 03:20:11 +05:00
agnostic-apollo
fbf55fd40c Fixed: Fix CSI parameters parsing like for SGR sequences that start with a ; or have sequential ; characters
https://vt100.net/docs/vt510-rm/chapter4.html#S4.3.3

https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences

Credits for finding the issue belongs to @Screwtapello

https://github.com/mawww/kakoune/issues/4339#issuecomment-916980723

Closes #2272, Closes mawww/kakoune#4339
2024-06-18 03:20:10 +05:00
agnostic-apollo
160ab68e5b Changed: Use black or white cursor color based on terminal background instead of always white if colors.properties didn't have cursor color set
Credit for algorithm link belong to @Jamie-Landeg-Jones

Closes #2653
2024-06-18 03:20:10 +05:00
agnostic-apollo
903f2496cb Fixed: Fix message dialog button text not showing in day mode due to white text 2024-06-18 03:20:10 +05:00
agnostic-apollo
2dc7381b89 Fixed: Fix wrong input type selected if toolbar is switched back to extra keys after tapping terminal if in text input mode
Closes #2503
2024-06-18 03:20:10 +05:00
agnostic-apollo
e55639e41d Fixed: Change extra keys and terminal input view background to black
Required for day/night theming and should fix issues where both views were translucent with light terminal color themes.
2024-06-18 03:20:10 +05:00
agnostic-apollo
087da0b576 Fixed: Fix issue where a colour tint/highlight would be added to the terminal on activity re-creation
The fix in c6b4114f was not working for it.
2024-06-18 03:20:10 +05:00
agnostic-apollo
d7f22982a1 Fixed: Fix termux app restarting on samsung dex version < 3.0 when switching modes 2024-06-18 03:20:10 +05:00
agnostic-apollo
f222315b0f Fixed: Fix ArrayIndexOutOfBoundsException when setting zero width terminal character
java.lang.ArrayIndexOutOfBoundsException: length=64; index=-1
at com.termux.terminal.TerminalRow.setChar(TerminalRow.java:127)
at com.termux.terminal.TerminalBuffer.setChar(TerminalBuffer.java:413)
at com.termux.terminal.TerminalEmulator.emitCodePoint(TerminalEmulator.java:2329)
at com.termux.terminal.TerminalEmulator.processCodePoint(TerminalEmulator.java:617)
at com.termux.terminal.TerminalEmulator.processByte(TerminalEmulator.java:513)
at com.termux.terminal.TerminalEmulator.append(TerminalEmulator.java:480)
at com.termux.terminal.TerminalSession$MainThreadHandler.handleMessage(TerminalSession.java:339)
at android.os.Handler.dispatchMessage(Handler.java:110)
at android.os.Looper.loop(Looper.java:219)
at android.app.ActivityThread.main(ActivityThread.java:8349)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055)
2024-06-18 03:20:10 +05:00
agnostic-apollo
e11bcfc9a1 Fixed: Log exception instead of crashing app on NumberFormatException for invalid termcap/terminfo string requested
java.lang.NumberFormatException: For input string: " a"
at java.lang.Long.parseLong(Long.java:583)
at java.lang.Long.valueOf(Long.java:781)
at java.lang.Long.decode(Long.java:933)
at com.termux.terminal.TerminalEmulator.doDeviceControl(TerminalEmulator.java:940)
at com.termux.terminal.TerminalEmulator.processCodePoint(TerminalEmulator.java:813)
2024-06-18 03:20:09 +05:00
agnostic-apollo
e3a50cbf32 Fixed: Use android.util.Log for terminal-emulator logging if TerminalSessionClient is null like when running tests 2024-06-18 03:20:09 +05:00
agnostic-apollo
af5fef4c4a Fixed: Fix CSI Delete Ps Column(s) (DECDC)
Firstly, `TerminalBuffer.blockSet()` was throwing the exception since `sx + w > mColumns` which was technically passed by TerminalEmulator.blockClear()`. Actual value would be `mCursorRow + columnsToMove + columnsToDelete > mColumns`.

Secondly, the call to `blockClear()` should not be needed since it the `blockCopy()` would overwrite the columns to be deleted on copy.

Run `printf "\e['~"` to delete 1 column and `printf "\e[3'~"` to delete 3 columns. Run `printf "\e[3'}"` to insert 2 columns.

java.lang.IllegalArgumentException: Illegal arguments! blockSet(78, 0, 1, 30, 32, 56, 30)
at com.termux.terminal.TerminalBuffer.blockSet(TerminalBuffer.java:397)
at com.termux.terminal.TerminalEmulator.blockClear(TerminalEmulator.java:2035)
at com.termux.terminal.TerminalEmulator.processCodePoint(TerminalEmulator.java:799)
2024-06-18 03:20:09 +05:00
agnostic-apollo
03e31d190d Fixed: Fix ArrayIndexOutOfBoundsException thrown because length was less than 0 when selecting text from terminal buffer
java.lang.ArrayIndexOutOfBoundsException: src.length=132 srcPos=90 dst.length=16 dstPos=0 length=-2
at java.lang.System.arraycopy(System.java:469)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:597)
at java.lang.StringBuilder.append(StringBuilder.java:191)
at com.termux.terminal.TerminalBuffer.getSelectedText(TerminalBuffer.java:97)
at com.termux.terminal.TerminalBuffer.getSelectedText(TerminalBuffer.java:57)
at com.termux.terminal.TerminalBuffer.getSelectedText(TerminalBuffer.java:53)
at com.termux.terminal.TerminalEmulator.getSelectedText(TerminalEmulator.java:2401)
at com.termux.view.textselection.TextSelectionCursorController$1.onActionItemClicked(TextSelectionCursorController.java:140)
2024-06-18 03:20:09 +05:00
agnostic-apollo
d24a04a10d Fixed: Fix issue where menu wouldn't show when text on bottom row of terminal was selected
Closes #2233
2024-06-18 03:20:09 +05:00
agnostic-apollo
aee0da49a0 Changed: Do not show toast if text null or empty 2024-06-18 03:20:09 +05:00
agnostic-apollo
87c8f3d35a Fixed: Fix NullPointerException when getting spanned markdown like for notification 2024-06-18 03:20:09 +05:00
agnostic-apollo
9259ef0be1 Changed: Bump to v0.118.0 2022-01-23 01:36:41 +05:00
agnostic-apollo
480f92880c Fixed: Fix bootstrap checksum check if it contained leading zeros 2022-01-23 01:36:41 +05:00
agnostic-apollo
b01a738791 Docs: Update README.md 2022-01-23 01:36:41 +05:00
agnostic-apollo
0eaaa1372a Changed: Bump bootstrap to v2022.01.07-r1 2022-01-23 01:36:41 +05:00
agnostic-apollo
903b1c75a2 Fixed: Fix bootstrap checksum check if it contained leading zeros 2022-01-23 01:36:41 +05:00
agnostic-apollo
085b17e496 Changed: Bump dependency versions 2022-01-23 01:36:41 +05:00
agnostic-apollo
897d911a52 Changed: Move to semantic versioning for app and library versions and add commit hash and github to APK file names
The `versionName` will now follow semantic version `2.0.0` spec in the format `major.minor.patch(-prerelease)(+buildmetadata)`. This will make versioning the prerelease and github debug builds versions easier and follow a spec. The @termux devs should make sure that when bumping `versionName` in `build.gradle` files and when creating a tag for new releases on github that they include the patch number as well, like `v0.1.0` instead of just `v0.1`. The `build.gradle` files and `attach_debug_apks_to_release` workflow will now validate the version as well and the build/attachment will fail if `versionName` does not follow the spec. https://semver.org/spec/v2.0.0.html

APKs released on github for debug build workflows and releases are now referred as `Github` releases as per 7b10a35f and 94e01d68, so APK filenames have been modified to include `github` in the filename. The APKs are still debuggable, so that tag remains too.

For github workflows the apk filename format will be `termux-app_<current_version>+<last_commit_hash>-github-debug_<arch>.apk`, like `termux-app_v0.1.0+xxxxxxxx-github-debug_arm64-v8a.apk` and for github releases it will be `termux-app_<release_version>+github-debug_<arch>.apk`, like `termux-app_v0.1+github-debug_arm64-v8a.apk`. The `last_commit_hash` will be the first `8` characters of the commit hash. The `<last_commit_hash>-github-debug` will act as `buildmetadata` and will not affect versioning precedence.

For github workflows triggered by `push` and `pull_request` triggers, `<current_version>+<last_commit_hash>` will be used as new `versionName`, like `v0.1.0+xxxxxxxx`. This will make tracking which build a user is using easier and help in resolving issues as well.

Note that users using github releases and termux devs using `$TERMUX_VERSION` environment variables in scripts should take commit hash into consideration and possibly use something like `dpkg --compare-versions "$TERMUX_VERSION" ge 0.1` where appropriate instead of mathematical comparisons.

The `app/build.gradle` now also supports following `TERMUX_` scoped environmental variables and `RELEASE_TAG` variable will not be used anymore since it may conflict with possibly other variables used by users. They will also allow enabling split APKs for both debug and release builds.

- `TERMUX_APP_VERSION_NAME` will be used as `versionName` if its set.
- `TERMUX_APK_VERSION_TAG` will be used as `termux-app_<TERMUX_APK_VERSION_TAG>_<arch>.apk` if its set. The `_<arch>` will only exist for split APKs.
- `TERMUX_SPLIT_APKS_FOR_DEBUG_BUILDS` will define whether split APKs should be enabled for debug builds. Default value is `1`.
- `TERMUX_SPLIT_APKS_FOR_RELEASE_BUILDS` will define whether split APKs should be enabled for release builds. Default value is `0` since F-Droid does not support split APKs, check #1904.

So based on above, if in future github releases are to be converted to `release` builds instead of `debug` builds, something like following can be done and even a workflow can be created for it. Users can also build split APKs release builds for themselves if they want.

```
export TERMUX_SPLIT_APKS_FOR_RELEASE_BUILDS=1
./gradlew assembleRelease -Pandroid.injected.signing.store.file="$(pwd)/app/dev_keystore.jks" -Pandroid.injected.signing.store.password=xrj45yWGLbsO7W0v -Pandroid.injected.signing.key.alias=alias -Pandroid.injected.signing.key.password=xrj45yWGLbsO7W0v
```

The APK will be found at `./app/build/outputs/apk/release/termux-app_<version>_<arch>.apk`

The `TERMUX_SPLIT_APKS_FOR_DEBUG_BUILDS` can be set to `0` to disable building split APKs which may be helpful for users building termux on device considering they will extra space and build time. Instructions for building are at https://github.com/termux/termux-packages/pull/7227#issuecomment-893022283.

```
export TERMUX_SPLIT_APKS_FOR_DEBUG_BUILDS=0
./gradlew assembleDebug
```

The APK will be found at `./app/build/outputs/apk/debug/termux-app_debug_universal.apk`

Note that F-Droid uses algorithm at https://gitlab.com/fdroid/fdroidserver/-/blob/2.1a0/fdroidserver/build.py#L746 to automatically detect built APKs, so ensure any modifications to location or file name are compliant. Current updates should be.

Auto updates are detected by checkupdates bot at https://gitlab.com/fdroid/fdroidserver/-/blob/master/fdroidserver/checkupdates.py
2022-01-23 01:36:41 +05:00
Henrik Grimler
cd5962c696 bootstrap archives: update to 2022.01.02-r1 2022-01-23 01:36:41 +05:00
Henrik Grimler
6e6da752bd Fixed: Fix copy&paste error in areHardwareKeyboardShortcutsDisabled
Fixes 829cc39868 ("Allow users to disable hardware keyboard
shortcuts").

Reported-by: @amogusissofunnyhahalmaogenzhumorbelike
2022-01-23 01:36:41 +05:00
Leonid Pliushch
6d60bc669b bootstrap archives: update to 2021.12.02-r1 2022-01-23 01:36:41 +05:00
Henrik Grimler
6c24e6ac3b termux-shared: add android.permission.VIBRATE to manifest
./gradlew lint complains about vibrations being used in
termux-shared/src/main/java/com/termux/shared/terminal/io/BellHandler.java
without the permission being declared.
2021-11-20 22:03:09 +01:00
YAKSH BARIYA
edf3b622e4 chore: Fix Discord server ID in shields.io badge
Based on dd59986a8c
2021-11-05 11:36:14 +05:30
agnostic-apollo
af16e79bf8 Merge pull request #2146 from trygveaa/click-on-url
Added: Allow users to directly open URL links in terminal transcript when clicked or tapped

The user can add `terminal-onclick-url-open=true` entry to `termux.properties` file to enable opening of URL links in terminal transcript when clicked or tapped. The default value is `false`. Running `termux-reload-settings` command will also update the behaviour instantaneously if changed.

Implemented in #2146
2021-10-13 22:36:52 +05:00
Leonid Pliushch
da6174e4c4 bootstrap archives: update to 2021.10.03-r1 2021-10-07 20:27:03 +03:00
agnostic-apollo
dcedf39434 Changed: Only allow ContentProvider access if allow-external-apps is set to true 2021-09-24 00:47:38 +05:00
agnostic-apollo
e302a14cd0 Fixed: Do not allow external apps to modify termux properties files with ContentProvider 2021-09-24 00:29:06 +05:00
agnostic-apollo
f3ffc36bfd Added: Add TermuxFileUtils.getExpandedTermuxPaths() and TermuxFileUtils.getUnExpandedTermuxPaths() 2021-09-23 21:40:37 +05:00
agnostic-apollo
1f0f80b0c9 Added: Add FileUtils.isPathInDirPaths() 2021-09-23 21:39:51 +05:00
agnostic-apollo
5e2bec0f4c Added: Add constants for launcher activities of termux plugins 2021-09-23 16:51:08 +05:00
agnostic-apollo
075a080f00 Added: Add functions to PackageUtils to check/modify app Component states
These can be used by Termux app and its plugin to disable launcher icons/activities if they are enabled at install time
2021-09-23 16:46:59 +05:00
agnostic-apollo
0bf4b1eca4 Added: Add Theme.MaterialComponents.DayNight.TermuxPrimaryActivity theme can be used by activities for day and night mode 2021-09-23 16:44:38 +05:00
agnostic-apollo
4f66786b98 Changed: Store termux-widget token synchronously to the SharedPreferences file on creation
Attempt to solve termux/termux-widget#16
2021-09-23 04:58:14 +05:00
agnostic-apollo
fefbf2ec03 Update LICENSE.md 2021-09-22 14:19:19 +05:00
Trygve Aaberge
54bb83de41 Fix calculation of row number for selection and URL clicking
When calculating the row that is clicked, for mouse tracking
mFontLineSpacingAndAscent was taken into account, but for selection and
URL clicking it wasn't. This adds a common function for calculating the
column and row which does take it into account and use that for all
three.

I'm not quite sure why it's necessary to subtract
mFontLineSpacingAndAscent, but with this calculation the click location
matches the line that is acted on for me with both touch and mouse and
on different font sizes.

It also removes the offset for finger the selection/url used because I
don't think it's common for apps on Android to have such an offset, and
because the mouse tracking did not use such an offset.
2021-09-19 18:10:46 +02:00
Trygve Aaberge
1a5a66d0ee Support clicking directly on a URL to open it
This allows you to click/press directly on a URL in the terminal view to
open it. It takes priority over opening the keyboard, so if you click on
a URL it is opened, and if you click anywhere else the keyboard opens
like before.

Currently, if the application in the terminal is tracking the mouse and
you click on a URL, both actions happen. The mouse event is sent to the
application, and the URL is also opened.

To enable support for this, you have to set
`terminal-onclick-url-open=true` in `termux.properties`.
2021-09-15 01:58:30 +02:00
agnostic-apollo
865f29d49a Added: Request android.permission.PACKAGE_USAGE_STATS permission
The permission can be granted from `Android Settings` -> `System` -> `Usage Access`.

Closes #2269
2021-09-12 15:50:52 +05:00
agnostic-apollo
22811167ac Update README.md 2021-09-12 10:06:01 +05:00
agnostic-apollo
819571a03a Update README.md 2021-09-11 19:07:59 +05:00
agnostic-apollo
c3280a94f0 Added: Add TextIOActivity and TextIOInfo
The `TextIOActivity` can be used to edit or view text based on various config options defined by `TextIOInfo`
and supports `monospace` font and horizontal scrolling for editing scripts, etc.

Current max text limit is `95KB`, which can be increased in future.
2021-09-11 15:15:51 +05:00
agnostic-apollo
5f3b1ccf90 Added: Add getDefaultIfUnset() to DataUtils and update comment 2021-09-11 15:15:51 +05:00
agnostic-apollo
0b47b20a9c Changed: Minor refactor and comment updates of ReportActivity and ReportInfo 2021-09-11 13:48:30 +05:00
agnostic-apollo
783a840e3a Added: Add MIN_VALUE_EXTRA_SESSION_ACTION and MAX_VALUE_EXTRA_SESSION_ACTION to TermuxConstants 2021-09-09 08:19:51 +05:00
agnostic-apollo
c19e01fc1b Changed!: Do not wait for the user to press enter for failed terminal session commands if plugin expects the result back 2021-09-09 07:25:43 +05:00
agnostic-apollo
9ffcd21ce1 Update README.md 2021-09-09 04:34:24 +05:00
agnostic-apollo
94e01d68d6 Update README.md 2021-09-08 17:53:23 +05:00
agnostic-apollo
0cf3cef7de Added: Add TERMUX_API_VERSION to termux shell environment
This can be used to check if `Termux:API` is installed and enabled for cases where users try to run `termux-api` commands and it hangs. The check can be added to start of each `termux-api` script during build time by replacing a placeholder with `sed`.

```
if dpkg --compare-versions "$TERMUX_VERSION" ge 0.118 && [ -z "$TERMUX_API_VERSION" ]; then
echo "The Termux:API app is not installed or enabled which is required by termux-api commands to work." 1>&2
exit 1
fi

current_user="$(id -un)"
termux_user="$(stat -c "%U" "/data/data/com.termux/files/usr")"
if [ "$current_user" != "$termux_user" ]; then
echo "The termux-api commands must be run as the termux user \"$termux_user\" instead of as \"$current_user\"." 1>&2
echo "Trying to run with \"su $termux_user -c termux-api-command\" will fail as well." 1>&2
exit 1
fi
```
2021-09-08 11:24:11 +05:00
agnostic-apollo
7b10a35f24 Changed!: Change TERMUX_IS_DEBUG_BUILD env variable name to TERMUX_IS_DEBUGGABLE_BUILD and change GITHUB_DEBUG_BUILD release type to just GITHUB
This is being done since github release artifacts may be converted to non-debuggable if felt appropriate in future or at least is a more appropriate name. Signing keys can stay same as per commit/push builds. Currently, no changes are planned, just future proofing. The `TERMUX_IS_DEBUGGABLE_BUILD` env variable could be used to differentiate if needed.

Will also check if Termux app is installed and not disabled and will calculate APK signature only when needed since its a slightly expensive operation.

This commit breaks da07826a.
2021-09-08 11:05:29 +05:00
agnostic-apollo
e36c5294db Changed: Only show system chooser if ActivityNotFoundException is thrown when opening url 2021-09-08 08:46:29 +05:00
agnostic-apollo
dd952a90ad Changed: Show system chooser if failed to find activity to handle url 2021-09-06 04:40:49 +05:00
agnostic-apollo
da07826a0c Added: Add TERMUX_IS_DEBUG_BUILD, TERMUX_APK_RELEASE and TERMUX_APP_PID to termux shell environment
The `TERMUX_IS_DEBUG_BUILD` env variable will be set to `1` if termux APK is a debuggable APK and `0` otherwise. Note that the `dev_keystore.jks` shipped with termux app and plugin source code can also be used to create a release APK even though its mainly used for Github Debug Builds, in which case value will be `0`.

The `TERMUX_APK_RELEASE` will be set to `GITHUB_DEBUG_BUILD`, `F_DROID` or `GOOGLE_PLAY_STORE` depending on release type. It will be set to `UNKNOWN` if signed with a custom key.

The `TERMUX_APP_PID` will be set to the process of the main app process of the termux app package (`com.termux`), assuming its running when shell is started, like for `termux-float`. This variable is included since `pidof com.termux` does not return anything for release builds. It does work for debug builds and over adb/root. However, you still won't be able to get additional process info with `ps`, like that of threads, even with the pid and will need to use adb/root. However, `kill $TERMUX_APP_PID` will work from `termux-app` and `termux-float`.

These variables can be used by termux devs and users for custom logic in future depending on release type.
2021-09-06 04:14:57 +05:00
agnostic-apollo
1259a212aa Changed: Make allowed custom log level added in 60f37bde to be more restrictive 2021-09-06 01:24:22 +05:00
agnostic-apollo
ac32fbc53d Added: Add SharedPreferences KEY_LAST_PENDING_INTENT_REQUEST_CODE for termux-tasker 2021-09-05 14:52:53 +05:00
agnostic-apollo
f00738fe3a Changed: Make sure full path is included in FileUtilsErrnos
Previously, `FileUtilsErrno` had some errors that didn't include the full path passed to the `FileUtils` functions and caller had to manually append the path to the error. This was done due to `termux-tasker` plugin config activity was using these errors in the executable and working directory text fields and we had to keep the error short as possible to reduce clutter. Now by default, the path will be included so that its not missing for other cases and the `FileUtils.getShortFileUtilsError()` function is provided to get a shorter version from the original error if its possible to do so if caller like `termux-tasker` requires it.
2021-09-05 10:09:18 +05:00
agnostic-apollo
5c72c3ca1b Added: Allow users to disable auto capitalization of extra keys text
The user can add `extra-keys-text-all-cap=false` entry to `termux.properties` file to disable auto capitalization of extra keys text for both normal and popup buttons. The default value is `true`. Running `termux-reload-settings` command will also update the behaviour instantaneously if changed.
2021-09-05 04:24:00 +05:00
agnostic-apollo
b62645cd03 Fixed: Fix typos and refactor 2021-09-05 03:37:07 +05:00
agnostic-apollo
23b707a819 Changed: Disable shrinkResources for testing reproducible builds 2021-09-04 08:34:32 +05:00
agnostic-apollo
4953b1269c Added: Add log level setting in Termux Settings for termux-widget 2021-09-04 08:33:30 +05:00
agnostic-apollo
d5ffb116b8 Added: Add constants and functions for termux-widget in TermuxConstants and TermuxPreferenceConstants 2021-09-04 08:08:51 +05:00
agnostic-apollo
e5c0548942 Added: Add isTermuxAppInstalled() and isTermuxAppAccessible() functions to TermuxUtils
The `TermuxUtils.isTermuxAppInstalled()` function can be used by external apps to check if termux app is installed and enabled.

The `TermuxUtils.isTermuxAppAccessible()` function can be used by termux plugin apps to check if termux app is installed, enabled, accessible as per `sharedUserId` and `TERMUX_PREFIX_DIR_PATH` is accessible and has read, write and execute permission.
2021-09-04 08:06:54 +05:00
agnostic-apollo
4e5f2c7e01 Changed/Fixed: Ensure bootstrap installation creates prefix and prefix staging directory before extraction
We manually create the parent directories first so that bootstrap failures are detected early on instead of some sub directory during extraction.

Also fixed issue where `TermuxFileUtils.isTermuxFilesDirectoryAccessible()` would not check if a directory file actually existed at TERMUX_FILES_DIR_PATH and may set permissions for a non-directory file at the path. The `TermuxInstaller` was testing if `TERMUX_PREFIX_DIR_PATH` existed later on so check wasn't necessary but function may be called from elsewhere too.

Also removed legacy `PREFIX_FILE*` and `STAGING_PREFIX_FILE*` local constants and use the ones provided by `TermuxConstants` directly.
2021-09-04 08:06:25 +05:00
agnostic-apollo
3373a1f41c Changed: Split long resource string on multiple lines 2021-09-04 07:25:21 +05:00
agnostic-apollo
52c1ee520f Added/Fixed: Add support to consider empty String values as null for SharedPreferences 2021-09-04 07:25:21 +05:00
agnostic-apollo
197979fdcc Fixed: Ensure custom log level doesn't log if its off or null 2021-09-04 05:28:39 +05:00
agnostic-apollo
bc779d2ffb Added: Add support for ~/.termux/termux.float.properties 2021-09-02 12:45:28 +05:00
agnostic-apollo
9f1203f049 Changed: Use multi-process SharedPrefernces for log level of plugin apps
Since termux-app runs in a separate process from other apps, if a user sets log level in termux settings, then it would require exiting the `termux-app` completely since android caches `SharedPrefernces` in memory and only writes to the file on app exit. Now updated value will be instantly written to the file so that plugins can directly read at startup. If plugins are already running, they would need to be restarted since usually log levels are loaded at startup.
2021-09-02 06:40:02 +05:00
agnostic-apollo
d55c1001c8 Added: Add termux-float log level settings in termux app settings 2021-09-02 06:21:16 +05:00
agnostic-apollo
36557b2166 Added: Add more SharedPrefernces for termux-float and use multi-process for log level 2021-09-02 06:20:39 +05:00
agnostic-apollo
1cf1e612e5 Added: Add constants for termux-float in TermuxConstants 2021-09-02 06:16:28 +05:00
agnostic-apollo
e7d06aebb5 Merge pull request #2237 from agnostic-apollo/extra-keys-conversion-to-agnosticism
Extra keys conversion to agnosticism and disabling hardware keyboard shortcuts and terminal margin customization support
2021-08-28 01:26:53 +05:00
agnostic-apollo
582e56938a Added: Add SharedPrefernces controllers for all current published termux plugin app
Also added log level setting in Termux Settings for Termux:API. Others can be added when logging is implemented in the plugin apps via `Logger` class provided by `termux-shared`.
2021-08-28 00:29:53 +05:00
agnostic-apollo
5a8c4f10ee Fixed|Changed: Fix TermuxFileReceiverActivity incorrect handling of intent extras
- If the `EXTRA_TEXT` value of the intent passed was empty instead of `null`, it was incorrectly assumed that text was passed, even though a valid `EXTRA_STREAM` may have been passed. Now `EXTRA_STREAM` will be checked first.
- Added empty extra and empty/`null` filename checks before trying to create a file with an empty filename and failing.
- Enable logging of intent passed at verbose log level.
- Changed to a better error dialog.

Closes #2247
2021-08-27 05:39:04 +05:00
agnostic-apollo
8387b70f64 Fixed: Fix terminal cursor blinker not stopping when typing a character in non-gboard keyboards 2021-08-26 06:03:23 +05:00
agnostic-apollo
994df1c4af Fixed|Added: Fix extra-keys shift key not uppercasing for all soft keyboards and added docs for keyboard key characters mapping 2021-08-26 06:01:25 +05:00
agnostic-apollo
63504f0adc Added: Allow users to adjust terminal horizontal and vertical margin
The `terminal-margin-horizontal` key can be used to adjust the terminal left/right margin and the `terminal-margin-vertical` can be used to adjust the terminal top/bottom margin. This will also affect drawer. The user can set an integer value between `0` and `100` as `dp` units. The default value is still `3` for horizontal and `0` for vertical margin. So adding an entry like `terminal-margin-horizontal=10` to `termux.properties` file will allow users to set a horizontal margin of `10dp`. After updating the value, either restart termux or run `termux-reload-settings` for changes to take effect.

This was added since for some users text on edges would not be shown on the screen or they had screen protectors/cases that covered screen edges (Of course, that would require fixing every single app and android system UI itself, so kinda stupid to use). Moreover, horizontal margin of like `10dp` may be helpful with peek-and-slide for people having gesture navigation enabled on android `10+` since they won't be to touch at exactly the edge of the screen to trigger peek (#1325).

Closes #2210
2021-08-25 23:18:17 +05:00
agnostic-apollo
829cc39868 Added: Allow users to disable hardware keyboard shortcuts
The user can add `disable-hardware-keyboard-shortcuts=true` entry to `termux.properties` file to disable hardware keyboard shortcuts. The default value is `false`. Running `termux-reload-settings` command will also update the behaviour instantaneously if changed. Note that for `ctrl+alt+p` to work, you need to unset `shortcut.rename-session = ctrl + n`. https://wiki.termux.com/wiki/Terminal_Settings

Closes #1825
2021-08-25 23:18:17 +05:00
agnostic-apollo
16c56a968e Changed|Fixed: Drawer extra-keys button will toggle instead of just opening
Also fixed NullPointerException due to changes in 2a74d43c
2021-08-25 23:18:17 +05:00
agnostic-apollo
b68a398fa8 Changed: Renamed typo TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION to TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION 2021-08-25 02:01:28 +05:00
agnostic-apollo
f97f07df3f Changed: Add selinux context info to termux files info of debug output 2021-08-23 21:51:43 +05:00
agnostic-apollo
c59835ed93 Revert "Changed: Bump compileSdkVersion to 31"
This reverts commit 296ee60d

We do not need to bump to compileSdkVersion 31 currently, since I have decided not to bump `androidx.window` to `1.0.0-alpha10` or higher currently, since it has changed APIs and ViewUtils will break.

https://developer.android.com/jetpack/androidx/releases/window

Moreover, bumping compileSdkVersion to 31 requires openjdk 11 in build environment, which will break Jitpack library build and possibly still F-Droid as well, unless changes are made.

https://gitlab.com/fdroid/fdroiddata/-/issues/2441

https://github.com/jitpack/jitpack.io/issues/4474

https://jitpack.io/docs/BUILDING/#java-version

Also in android studio stub files are loaded when opening class sources since android 12 sources aren't available.
2021-08-23 14:59:02 +05:00
agnostic-apollo
d1478fb6c3 Fixed: Ensure FN extra key is read by the terminal
Can't find info on why it wasn't being read before
2021-08-23 08:56:36 +05:00
agnostic-apollo
9117240961 Added: Add shift key support in extra keys and terminal with SHIFT or SHFT
Closes #1038
2021-08-23 08:51:30 +05:00
agnostic-apollo
fbb91149b5 Fixed: Use default values if extra-keys or extra-keys-style termux.properties values are empty 2021-08-23 08:48:25 +05:00
agnostic-apollo
2a74d43ca5 Added!: Convert extra-keys to agnosticism
The termux `extra-keys` have been moved to `termux-shared` library so that they can be imported and used by other apps for their own needs as long as they comply with GPLv3 license.

Almost everything is customizable and has no dependency on termux specific logic. Check the javadocs of files of `com.termux.shared.terminal.io.extrakeys` package for more info, specially, `ExtraKeysView`, `ExtraKeysInfo`, `ExtraKeyButton`, `TerminalExtraKeys` and  `TermuxTerminalExtraKeys`.

Moreover, you can now long hold on `CTRL`, `ALT`, `SHIFT` and `FN` to lock those control keys. They will not be released when you press another key and will only be released by pressing the respective control key again.

Closes #2049, Closes #1861
2021-08-23 08:48:24 +05:00
Leonid Pliushch
f65f384acf Merge pull request #2228 from termux/cursor-colors
terminal: invert text color under block cursor
2021-08-21 11:39:32 +03:00
agnostic-apollo
f055305790 Changed: Bump gradle to 7.2 2021-08-21 06:01:58 +05:00
agnostic-apollo
296ee60dc8 Changed: Bump compileSdkVersion to 31
Needed since gradle library dependencies have minCompileSdk set to 31
2021-08-21 05:48:00 +05:00
agnostic-apollo
1c01f4df08 Changed: Bump gradle library dependency versions 2021-08-21 05:33:00 +05:00
agnostic-apollo
e889d84dc4 Changed!: Changes introduced to disable/change logging in 60f37bde now also apply to stdin and plugin command results 2021-08-21 05:26:33 +05:00
agnostic-apollo
956e20e53d Fixed: Fix NullPointerException when running bell/vibrate on Samsung devices on android 8 and handled deprecated code
Apparently occurs on only Samsung android 8 devices and there is no fix for vibrator except catching the exception so that app doesn't crash.

https://gitlab.com/juanitobananas/wave-up/-/issues/131
https://github.com/overbound/SonicTimeTwisted/issues/131
https://web.archive.org/web/20201114040257/https://www.badlogicgames.com/forum/viewtopic.php?t=28507

```
java.lang.NullPointerException: Attempt to read from field 'android.os.VibrationEffect com.android.server.VibratorService$Vibration.mEffect' on a null object reference
at android.os.Parcel.readException(Parcel.java:2035)
at android.os.Parcel.readException(Parcel.java:1975)
at android.os.IVibratorService$Stub$Proxy.vibrate(IVibratorService.java:292)
at android.os.SystemVibrator.vibrate(SystemVibrator.java:81)
at android.os.Vibrator.vibrate(Vibrator.java:191)
at android.os.Vibrator.vibrate(Vibrator.java:110)
at android.os.Vibrator.vibrate(Vibrator.java:89)
at com.termux.app.terminal.io.BellHandler$1.run(BellHandler.java:37)
at com.termux.app.terminal.io.BellHandler.doBell(BellHandler.java:55)
at com.termux.app.terminal.TermuxTerminalSessionClient.onBell(TermuxTerminalSessionClient.java:178)
at com.termux.terminal.TerminalSession.onBell(TerminalSession.java:278)
```
2021-08-21 04:22:43 +05:00
agnostic-apollo
10704b1dad Changed: Use extended version of Logger functions for logging execution commands 2021-08-21 03:48:32 +05:00
agnostic-apollo
19f4084099 Added: Add labels for ExecutionCommand for termux internal commands 2021-08-21 03:41:29 +05:00
agnostic-apollo
486faf7fad Fixed: Stdin not being logged for background execution commands 2021-08-21 03:40:31 +05:00
agnostic-apollo
6409019a40 Added: Add warning that hax support is not provided and asking questions will likely result in issue automatically closed or even ban 2021-08-21 02:55:13 +05:00
agnostic-apollo
7047bbefbb Added: Add warning reports with (partial) screenshots of error reports instead of text will likely be automatically closed/deleted 2021-08-21 02:47:13 +05:00
agnostic-apollo
24ea83d6c0 Added: Bootstrap error and report issue (optionally) will contain primary termux files stat info and logcat dump
Users have been reporting issues with bootstrap installation (and `login` file access) failure on email and github but "most" have been useless since they don't follow instructions to debug the issue and report back. The real reason may depend on device. One could be that `/data/data/com.termux` does not exist on the device in which case termux won't work on the device, at least without root. Other reasons could be wrong ownership or selinux context, selinux denials or attempting to install on external sd card (as reported by a user) where likely files dir was different from `/data/data/com.termux/files`.

This commit will save dev and possibly user time and automatically generate the required info to debug such issues. The `ls` command will generate `stat` info for all the major termux directories and files so that existence or ownership issues can be shown. It will also run `logcat` command to take a dump (last `3000` lines) in case other failures are being logged, like selinux denials as per `avc` entries. It will also show if app is installed on external sd card. This info will automatically be shown on bootstrap install failure report.

Moreover, users can generate termux files `stat` info and `logcat` dump manually too with terminal's long hold options menu `More` -> `Report Issue` option and selecting `YES` in the prompt shown to add debug info. This can be helpful for reporting and debugging other issues. If the report generated is too large, then `Save To File` option in context menu (3 dots on top right) of `ReportActivity` can be used and the file viewed/shared instead.

Users must post complete report (optionally without sensitive info) when reporting issues, instead of (partial) screenshots which won't be accepted anymore.

There has been some design changes in android 11 for `/data/data` and `/data/user/0` directory. You can check javadoc for `isTermuxFilesDirectoryAccessible()` function in [`TermuxFileUtils`](termux-shared/src/main/java/com/termux/shared/file/TermuxFileUtils.java) for details.
2021-08-21 02:44:51 +05:00
agnostic-apollo
351934a619 Added|Fixed!: Added support to save reports to files and fixed large reports generating TransactionTooLargeException
If `ReportActivity` was started with a large report, i.e a few hundred `KB`, like for terminal transcript or other command output, the activity start would fail. To solve the issue, if the serialized size of the ReportInfo info object is above `DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES` (`100KB`), it will be saved to a file in a cache directory `/data/data/com.termux/cache/report_activity` as a serialized object and loaded when activity is started. The file will be automatically deleted when activity is destroyed (`Activity.onDetroy()`) or when notification that would have started the activity is deleted (`Notification.deleteIntent`). In case, these two didn't happen, then on `TermuxActivity` startup, a thread will be started to delete files older than `14` days so that unneeded left over files are deleted. If user tries to open plugin error or crash report notifications after 14 days, they will get `ReportInfo` file not found errors, assuming `TermuxActivity` was started to run the cleanup routine.

Now these large reports can't be copied or shared with other apps since that would again result in `TransactionTooLargeException` exceptions and `ShareUtils` automatically truncates the data (now from end) to `100KB` length so that the exception doesn't occur. So now a `Save To File` option has been added in context menu (3 dots on top right) of `ReportActivity` so that large or small reports can be saved to a file if needed. They will be save in root of `/storage/emulated/0` or whatever is the default public external storage directory. The filename would depend on type of report. The storage permissions will be asked if missing. On android `11`, if you get permission denied errors even after granting permission, disable permission and grant it again. To solve privacy issues of report being saved to public storage since it may contain private info, an option for custom path will be added in future. The default directory is public storage instead of termux home since its easily accessible via all file managers or from pc. Instructing amateur users to get files via `SAF` from termux home is not something I wanna take on.

Another issue is that `ReportActivity` itself may not be able to show the entire report since Android may throw `OutOfMemoryError` exceptions if device memory is low. To solve this issue, `ReportActivity` will truncate the report to `1MB` from end that's shown to the user. It will add a header showing that report was truncated. To view the full report, the user will have to use the `Save To File` option and view the file in an external app or on pc that supports opening large files. The `QuickEdit` app on Android has been a reliable one in my experience that supports large files, although it has max row/column limits too at a few hundred thousand, depending on android version.

Despite all this, `OutOfMemoryError` exceptions could still be thrown if you try to view too large a report, like a few MB, since original report + the truncated report is still held in memory by the app and will consume `2-3` times memory when saving. It's fun coding for android, right?

The terminal transcript will not be truncated anymore that's generated via `Report Issue` option in terminal.

The `ShareUtils.copyTextToClipboard()` will truncate data now automatically, apparently all phones don't do it automatically and exception is raised.

The `ShareUtils.saveTextToFile()` has been added that will automatically ask for storage permissions if missing.

The `ReportInfo` now expects a `reportSaveFileLabel` and `reportSaveFilePath` arguments so that `ReportActivity` can use them to know where to save the file if users selects `Save To File` option.

The `ReportActivityBroadcastReceiver` must now be registered in `AndroidManifest.xml` if you are using `ReportActivity` in your app. Check `ReportActivity` javadoc for details. Moreover, an incremental call to `ReportActivity.deleteReportInfoFilesOlderThanXDays()` must also be made.
2021-08-20 23:31:12 +05:00
agnostic-apollo
e7fc60af72 Fixed: New plugin error or crash notifications overriding content of old ones 2021-08-20 22:20:18 +05:00
agnostic-apollo
baacabdfbf Added!: Support for delete intent for Notification.Builder in NotificationUtils 2021-08-20 22:12:24 +05:00
agnostic-apollo
35ea19dd75 Added: Support for reading and writing serialized objects to files and deleting files older than x days in FileUtils 2021-08-20 06:36:01 +05:00
agnostic-apollo
7de0613617 Fixed: Catch exception when requesting permissions, like if request code is negative 2021-08-20 06:19:25 +05:00
agnostic-apollo
5e09a501c9 Added: Support for MessageDialogUtils.showMessage() to receive positive and negative button OnClickListeners 2021-08-20 06:19:25 +05:00
agnostic-apollo
60f37bde8d Changed!: StreamGobbler needs to be passed log level parameter
When `Logger.CURRENT_LOG_LEVEL` set by user is `Logger.LOG_VERBOSE`, then background (not foreground sessions) command output was being logged to logcat, however, if command outputted too much data to logcat, then logcat clients like in Android Studio would crash. Also if a logcat dump is being taken inside termux, then duplicate lines would occur, first one due to of original entry, and second one due to StreamGobbler logging output at verbose level for logcat command.

This would be a concern for plugins as well like `RUN_COMMAND` intent or Termux:Tasker, etc if they ran commands with lot of data and user had set log level to verbose.

For plugins, TermuxService now supports `com.termux.execute.background_custom_log_level` `String` extra for custom log level. Termux:Tasker, etc will have to be updated with support. For `RUN_COMMAND` intent, the `com.termux.RUN_COMMAND_BACKGROUND_CUSTOM_LOG_LEVEL` `String` extra is now provided to set custom log level for only the command output. Check `TermuxConstants`.

So one can pass a custom log level that is `>=` to the log level set it termux settings where (OFF=0, NORMAL=1, DEBUG=2, VERBOSE=3). If you pass `0`, it will completely disable logging. If you pass `1`, logging will only be enabled if log level in termux settings is `NORMAL` or higher. If custom log level is not passed, then old behaviour will remain and log level in termux settings must be `VERBOSE` or higher for logging to be enabled. Note that the log entries will still be logged with priority `Log.VERBOSE` regardless of log level, i.e `logcat` will have `V/`.

The entries logcat component has now changed from `StreamGobbler` to `TermuxCommand`. For output at `stdout`, the entry format is `[<pid>-stdout] ...` and for the output at `stderr`, the entry format is `[<pid>-stderr] ...`. The `<pid>` will be process id as an integer that was started by termux. For example: `V/TermuxCommand: [66666-stdout] ...`.

While doing this I realize that instead of using `am` command to send messages back to tasker, you can use tasker `Logcat Entry` profile event to listen to messages from termux at both `stdout` and `stderr`. This might be faster than `am` command intent systems or at least possibly more convenient in some use cases.

So setup a profile with the `Component` value set to `TermuxCommand` and `Filter` value set to `-E 'TermuxCommand: \[[0-9]+-((stdout)|(stderr))\] message_tag: .*'` and enable the `Grep Filter` toggle so that entry matching is done in native code. Check https://github.com/joaomgcd/TaskerDocumentation/blob/master/en/help/logcat%20info.md for details. Also enable `Enforce Task Order` in profile settings and set collision handling to `Run Both Together` so that if two or more entries are sent quickly, entry task is run for all. Tasker currently (v5.13.16) is not maintaining order of entry tasks despite the setting.

Then you can send an intent from tasker via `Run Shell` action with `root` (since `am` command won't work without it on android >=8) or normally in termux from a script, you should be able to receive the entries as `@lc_text` in entry task of tasker `Logcat Entry` profile. The following just passes two `echo` commands to `bash` as a script via `stdin`. If you don't have root, then you can call a wrapper script with `TermuxCommand` function in `Tasker Function` action that sends another `RUN_COMMAND` intent with termux provide `am` command which will work without root.

```
am startservice --user 0 -n com.termux/com.termux.app.RunCommandService -a com.termux.RUN_COMMAND --es com.termux.RUN_COMMAND_PATH '/data/data/com.termux/files/usr/bin/bash' --es com.termux.RUN_COMMAND_STDIN 'echo "message_tag: Sending message from tasker to termux"' --ez com.termux.RUN_COMMAND_BACKGROUND true --es com.termux.RUN_COMMAND_BACKGROUND_CUSTOM_LOG_LEVEL '1'
```
2021-08-20 06:19:25 +05:00
agnostic-apollo
fabcc4fa35 Fixed: RunCommandService notification was not being cleared if an error was raised 2021-08-20 06:19:25 +05:00
agnostic-apollo
98edf1fbc7 Changed: Use millisecond timestamps for reports 2021-08-20 06:19:25 +05:00
agnostic-apollo
8ee0c5a6ec Fixed: Fix markdown link generation
The `]` characters in label and `)` characters in url must be escaped.
2021-08-20 06:19:25 +05:00
Leonid Pliushch
4a74618f17 terminal: set default cursor color to white 2021-08-19 16:44:28 +03:00
Leonid Pliushch
19c6134c71 terminal: invert text color under block cursor
Issue: https://github.com/termux/termux-app/issues/219
2021-08-19 16:29:12 +03:00
Leonid Pliushch
501d13a0cb update bootstrap archives 2021-08-18 00:10:40 +03:00
Henrik Grimler
e13773fd83 bug-report template: format text a bit 2021-08-17 09:51:34 +02:00
Henrik Grimler
23d2c1f0e9 github: convert issue templates to forms 2021-08-17 09:48:43 +02:00
agnostic-apollo
cac9a769c0 Merge pull request #2217 from the-blank-x/supportgemini
Add gemini to the list of url regex protocols
2021-08-11 23:42:20 +05:00
blank X
e30812af22 Add Gemini to the list of protocols 2021-08-12 00:34:21 +07:00
agnostic-apollo
1578ab5547 Merge pull request #2199 from WMCB-Tech/master
README: use the latest discord invite badge
2021-08-01 17:39:37 +05:00
marcusz
d5d87639ce update the discord badge link to updated one 2021-08-01 20:27:49 +08:00
agnostic-apollo
8ba5458221 Merge pull request #2198 from WMCB-Tech/master
Add Discord badge link
2021-08-01 14:28:09 +05:00
marcusz
a7596e7d03 add "join the discord" badge
discord may be the popular platform and bridged through gitter/irc. so i guess why not
2021-08-01 17:17:26 +08:00
agnostic-apollo
2b7aa5e803 Fix issue where wrong IME inputType would be set if termux was returned to from another app with text input view mode selected 2021-07-30 00:32:46 +05:00
agnostic-apollo
2b386efc3c Update strings.xml 2021-07-27 21:20:47 +05:00
TotalCaesar659
9a306ca1c5 readme: update urls to https (#2190) 2021-07-27 01:44:28 +03:00
agnostic-apollo
9febca9567 Fix comment 2021-07-19 18:12:57 +05:00
agnostic-apollo
7d76e8b185 Add PASTE extra key for pasting text from clipboard 2021-07-19 17:52:11 +05:00
agnostic-apollo
00d80b9e02 Automatically attach debug APKs when a release is created 2021-07-16 17:08:26 +05:00
agnostic-apollo
f10de462d2 Fix app packaging warning
PackagingOptions.jniLibs.useLegacyPackaging should be set to true because android:extractNativeLibs is set to "true" in AndroidManifest.xml.

https://monitor.f-droid.org/builds/log/com.termux/117
2021-07-16 17:05:43 +05:00
agnostic-apollo
f837ddef23 Bump gradle to 4.2.2 2021-07-16 14:40:22 +05:00
agnostic-apollo
f4e70678b1 Ensure that markdown code formatting is not broken for ResultSender if data itself contains any backticks 2021-07-14 17:39:05 +05:00
agnostic-apollo
a189f63604 Ensure failsafe session can still be opened if files directory is not accessible and fix comment
The `/data/data/com.termux` directory will not be created if it did not already exist and android did not already create it instead of as mentioned in 6fa4b9b7. Check https://github.com/termux/termux-app/issues/2168#issuecomment-879705552
2021-07-14 13:37:25 +05:00
Leonid Pliushch
0308d6a6ca extra keys: avoid scheduled executor leak
Under certain cases scheduled executor may leak causing repeatable input to
stuck.

Issue: https://github.com/termux/termux-app/issues/2156
2021-07-11 18:17:19 +03:00
Leonid Pliushch
1b62f7c9a9 installer: fix permissions for lib/apt/apt-helper
It should have executable bit set, otherwise it won't be possible to use tools such as 'apt-file' without reinstalling 'apt'.
2021-07-10 18:25:50 +03:00
agnostic-apollo
6fa4b9b7cd Ensure termux files directory is accessible before bootstrap installation and provide better info when running as secondary user/profile
Termux will check if termux files directory `/data/data/com.termux/files` has rwx permission access before installing bootstrap or starting terminal. Missing permission will automatically be set if possible. The `/data/data/com.termux` directory will also be created if it did not already exist, like if android did not already create it.

Users will now also be shown a crash notification if they attempt to start termux as a secondary user or in a work profile with info of the "alternate" termux files directory `/data/user/<id>/com.termux` set by android and the profile owner app if running under work profile (not secondary user). A notification will also be shown if the termux files directory (not "alternate") is not accessible.

Related #2168
2021-07-10 16:00:28 +05:00
agnostic-apollo
b2a071aad9 Update trigger_library_builds_on_jitpack.yml 2021-07-09 11:14:11 +05:00
agnostic-apollo
9272a757af Bump to v0.117 2021-07-08 13:12:31 +05:00
agnostic-apollo
d49fd6b00c Trigger termux library builds on jitpack on releases 2021-07-08 13:10:50 +05:00
agnostic-apollo
e0ad9ff573 Allow users to disable terminal margin adjustment from termux settings
Previously in (32135025) support was added with `disable-terminal-margin-adjustment` `termux.properties` property to disable terminal margin adjustment in case in causes screen flickering or other issues on some devices. It has now been removed in (7aefd943) and moved to Termux Settings since if it causes issues at startup and users can't access `termux.properties` file from the terminal, they will have to use SAF or root to access it, which will require an external app.

Users can set the value from the `Termux Settings` -> `Termux` -> `Terminal View` -> `Terminal Margin Adjustment` toggle. The `Termux Settings` can be accessed from left drawer in termux and from the android launcher shortcut for Termux Settings, usually accessible by long holding on Termux icon.
2021-07-08 12:17:49 +05:00
agnostic-apollo
7aefd94369 Revert "Allow users to disable terminal margin adjustment"
This reverts commit 32135025
2021-07-08 11:24:29 +05:00
agnostic-apollo
dc8bdfe675 Attempt to fix bootstrap installation failure that may be caused by invalid mkdirs return value 2021-07-08 10:50:30 +05:00
agnostic-apollo
c6b4114f86 Fix issue where a colour tint/highlight would be added to the terminal
This would happen when soft keyboard was to be disabled or hidden at startup and a hardware keyboard was attached and user started typing on hardware keyboard without tapping on the terminal first.
2021-07-08 10:01:47 +05:00
agnostic-apollo
cce6dfed22 Fix issue where RUN_COMMAND intent was failing for coreutils/busybox applets 2021-07-08 09:20:25 +05:00
agnostic-apollo
56c3826680 Add app and device info too for crash notification shown when bootstrap installation or setup storage fails 2021-07-08 08:49:32 +05:00
agnostic-apollo
2cf21c8409 Update .gitignore 2021-07-08 08:28:31 +05:00
agnostic-apollo
4361c5e0c5 Fix java.lang.AbstractMethodError: androidx.window.sidecar.SidecarInterface$SidecarCallback.onDeviceStateChanged
The crash was reported for `Microsoft Surface Duo`, which would affect some samsung and other devices as well, mainly dual screens/foldables. It was caused by androidx:window library that has been used by termux-shared since v0.115 having a typo in its proguard rules which didn't stop the removal of the required method for release builds (not debug) by proguard.

The library has been patched and fix should be available on next version but doing an emergency patch now for termux as well.

For people who are getting the crash should set `disable-terminal-margin-adjustment=true` in `termux.properties` created as per instructions in the link below and then start termux again and see if it fixes the issue. If you had termux installed before updating, you should be able to directly access the `~/.termux/termux.properties` file with SAF.

https://github.com/termux/termux-app/issues/1896#issuecomment-766188879

------

**Crash Message**:
```
abstract method "void androidx.window.sidecar.SidecarInterface$SidecarCallback.onDeviceStateChanged(androidx.window.sidecar.SidecarDeviceState)"
```

### Stacktrace

```
java.lang.AbstractMethodError: abstract method "void androidx.window.sidecar.SidecarInterface$SidecarCallback.onDeviceStateChanged(androidx.window.sidecar.SidecarDeviceState)"
at androidx.window.sidecar.MicrosoftSurfaceSidecar.updateDeviceState(MicrosoftSurfaceSidecar.java:159)
at androidx.window.sidecar.MicrosoftSurfaceSidecar$1.deviceStateChanged(MicrosoftSurfaceSidecar.java:192)
at android.vendor.screenlayout.service.IWindowExtensionCallbackInterface$Stub.onTransact(IWindowExtensionCallbackInterface.java:94)
at android.os.Binder.execTransactInternal(Binder.java:1021)
at android.os.Binder.execTransact(Binder.java:994)

```

https://issuetracker.google.com/issues/189001730
https://android-review.googlesource.com/c/platform/frameworks/support/+/1757630
2021-07-08 08:27:44 +05:00
agnostic-apollo
a53cc88688 Bump gradle dependencies versions 2021-07-08 08:14:42 +05:00
agnostic-apollo
48161816e0 Merge pull request #2163 from arib21/patch-1
Fixed grammar in the README.md file...
2021-07-07 15:39:14 +05:00
Arib Muhtasim
eabbda8efd Fixed grammar in the README.md file...
Went through the README.md file and fixed a lot of grammatical mistakes.
I know this is useless but I was bored...
2021-07-07 16:31:33 +06:00
agnostic-apollo
b90d59479a Fix typo in dccd155 2021-07-02 06:29:05 +05:00
agnostic-apollo
dccd155ba6 Enable split apks for debug builds
APKs for each architecture and a universal APK that is compatible for all architectures will now be available from Github Actions page from the workflow runs labeled `Build`. The APKs will be available as zips under the Artifact section named `termux-app-*`.

Architecture specific APKs can be used by users with low disk space since F-Droid releases are universal (since it doesn't support split APKs #1904) and their install+bootstrap installation size is ~180MB instead of ~120MB if an architecture specific APK is used.

This should also reduce bandwidth usage and download time for debug builds users if they download an architecture specific zip instead of the universal one.

Related #2153
2021-07-02 06:14:38 +05:00
agnostic-apollo
78be0e793e Update README.md 2021-07-01 11:17:47 +05:00
agnostic-apollo
e547c15481 Bump to v0.116 2021-07-01 10:52:51 +05:00
agnostic-apollo
c621c35827 Bump to v0.115 2021-07-01 08:36:03 +05:00
agnostic-apollo
886e52dcff Export JITPACK_NDK_VERSION for jitpack
Jitpack build is failing with the following error

```
> Configure project :
Gradle version Gradle 7.1

FAILURE: Build failed with an exception.

* What went wrong:
A problem occurred configuring project ':app'.
> com.android.builder.errors.EvalIssueException: NDK from ndk.dir at /opt/android-sdk-linux/ndk-bundle had version [21.1.6352462] which disagrees with android.ndkVersion [22.1.7171670]
```

So attempting to manually export an env variable for jitpack which uses ndk 21.1.6352462 instead of the termux default 22.1.7171670 and which is also used by F-Droid

https://jitpack.io/com/github/termux/termux-app/0.115/build.log

https://github.com/jitpack/jitpack.io/blob/master/BUILDING.md#custom-commands

https://gitlab.com/fdroid/fdroiddata/-/blob/master/metadata/com.termux.yml#L726
2021-07-01 08:34:00 +05:00
agnostic-apollo
8e4da6cbcd Revert "Bump to v0.115"
This reverts commit bde9d01f
2021-07-01 08:29:19 +05:00
140 changed files with 7748 additions and 1559 deletions

View File

@@ -0,0 +1,44 @@
name: "Bug report"
description: "Create a report to help us improve"
title: "[Bug]: "
labels: ["bug report"]
body:
- type: markdown
attributes:
value: |
This is a bug tracker of the Termux app. If you have issues with a package inside the app, then please open an issue at [termux-packages](https://github.com/termux/termux-packages) instead.
Use search before you open an issue to check whether your issue has been already reported and perhaps solved.
Android versions 5.x and 6.x are not supported anymore.
If you have issues installing packages then please see https://github.com/termux/termux-packages/issues/6726.
- type: textarea
attributes:
label: Problem description
description: |
A clear and concise description of what the problem is. You may attach the logs, screenshots, screen video recording and whatever else that will help to understand the issue.
Issues without proper description will be closed without solution.
validations:
required: true
- type: textarea
attributes:
label: Steps to reproduce the behavior.
description: |
Please post all necessary commands that are needed to reproduce the issue.
validations:
required: true
- type: textarea
attributes:
label: What is the expected behavior?
- type: textarea
attributes:
label: System information
description: Please provide info about your device
value: |
* Termux application version:
* Android OS version:
* Device model:
validations:
required: true

View File

@@ -0,0 +1,19 @@
name: "Feature request"
description: "Suggest a new feature for Termux application"
title: "[Feature]: "
labels: ["feature request"]
body:
- type: textarea
attributes:
label: Feature description
description: Describe the feature and why you want it.
validations:
required: true
- type: textarea
attributes:
label: Additional information
description: |
Does another app/terminal emulator have this feature?
Provide links to more background information.
validations:
required: true

View File

@@ -1,35 +0,0 @@
---
name: Bug report
about: Create a report to help us improve Termux application
---
<!--
IMPORTANT:
1. Support of Android 5.x - 6.x is finished.
2. Fill the template AFTER comments.
-->
**Problem description**
<!--
A clear and concise description of what the problem is.
You may post screenshots in addition to description.
-->
**Steps to reproduce**
<!--
Steps to reproduce the behavior. Please post all necessary
commands that are needed to reproduce the issue.
-->
**Expected behavior**
<!--
A clear and concise description of what you expected to happen.
-->
**Additional information**
* Termux application version:
* Android OS version:
* Device model:

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Want ask questions about the project?
url: https://github.com/termux/termux-app/discussions
about: Join GitHub Discussions

View File

@@ -1,22 +0,0 @@
---
name: Feature request
about: Suggest a new feature for Termux application
---
<!--
IMPORTANT:
1. Support of Android 5.x - 6.x is finished.
2. Fill the template AFTER comments.
-->
**Feature description**
<!--
Describe the feature and why you want it.
-->
**Reference implementation**
Does another app/terminal emulator have this feature?
Provide links to more background information.

View File

@@ -0,0 +1,75 @@
name: Attach Debug APKs To Release
on:
release:
types:
- published
jobs:
attach-apks:
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Clone repository
uses: actions/checkout@v2
with:
ref: ${{ env.GITHUB_REF }}
- name: Build and attach APKs to release
shell: bash {0}
run: |
exit_on_error() {
echo "$1"
echo "Deleting '$RELEASE_VERSION_NAME' release and '$GITHUB_REF' tag"
hub release delete "$RELEASE_VERSION_NAME"
git push --delete origin "$GITHUB_REF"
exit 1
}
echo "Setting vars"
RELEASE_VERSION_NAME="${GITHUB_REF/refs\/tags\//}"
if ! printf "%s" "${RELEASE_VERSION_NAME/v/}" | grep -qP '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'; then
exit_on_error "The versionName '${RELEASE_VERSION_NAME/v/}' is not a valid version as per semantic version '2.0.0' spec in the format 'major.minor.patch(-prerelease)(+buildmetadata)'. https://semver.org/spec/v2.0.0.html."
fi
APK_DIR_PATH="./app/build/outputs/apk/debug"
APK_VERSION_TAG="$RELEASE_VERSION_NAME+github-debug"
APK_BASENAME_PREFIX="termux-app_$APK_VERSION_TAG"
echo "Building APKs for '$RELEASE_VERSION_NAME' release"
export TERMUX_APK_VERSION_TAG="$APK_VERSION_TAG" # Used by app/build.gradle
if ! ./gradlew assembleDebug; then
exit_on_error "Build failed for '$RELEASE_VERSION_NAME' release."
fi
echo "Validating APKs"
for abi in universal arm64-v8a armeabi-v7a x86_64 x86; do
if ! test -f "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_$abi.apk"; then
files_found="$(ls "$APK_DIR_PATH")"
exit_on_error "Failed to find built APK at '$APK_DIR_PATH/${APK_BASENAME_PREFIX}_$abi.apk'. Files found: "$'\n'"$files_found"
fi
done
echo "Generating sha25sums file"
if ! (cd "$APK_DIR_PATH"; sha256sum \
"${APK_BASENAME_PREFIX}_universal.apk" \
"${APK_BASENAME_PREFIX}_arm64-v8a.apk" \
"${APK_BASENAME_PREFIX}_armeabi-v7a.apk" \
"${APK_BASENAME_PREFIX}_x86_64.apk" \
"${APK_BASENAME_PREFIX}_x86.apk" \
> "${APK_BASENAME_PREFIX}_sha256sums"); then
exit_on_error "Generate sha25sums failed for '$RELEASE_VERSION_NAME' release."
fi
echo "Attaching APKs to github release"
if ! gh release upload "$RELEASE_VERSION_NAME" \
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_universal.apk" \
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_arm64-v8a.apk" \
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_armeabi-v7a.apk" \
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_x86_64.apk" \
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_x86.apk" \
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_sha256sums" \
; then
exit_on_error "Attach APKs to release failed for '$RELEASE_VERSION_NAME' release."
fi

View File

@@ -4,23 +4,111 @@ on:
push: push:
branches: branches:
- master - master
- android-10 - 'github-releases/**'
pull_request: pull_request:
branches: branches:
- master - master
- android-10
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Clone repository - name: Clone repository
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Build
run: | - name: Build APKs
./gradlew assembleDebug shell: bash {0}
- name: Store generated APK file run: |
uses: actions/upload-artifact@v2 exit_on_error() { echo "$1"; exit 1; }
with:
name: termux-app echo "Setting vars"
path: ./app/build/outputs/apk/debug # Set RELEASE_VERSION_NAME to "<CURRENT_VERSION_NAME>+<last_commit_hash>"
CURRENT_VERSION_NAME_REGEX='\s+versionName "([^"]+)"$'
CURRENT_VERSION_NAME="$(grep -m 1 -E "$CURRENT_VERSION_NAME_REGEX" ./app/build.gradle | sed -r "s/$CURRENT_VERSION_NAME_REGEX/\1/")"
RELEASE_VERSION_NAME="v$CURRENT_VERSION_NAME+${GITHUB_SHA:0:7}" # The "+" is necessary so that versioning precedence is not affected
if ! printf "%s" "${RELEASE_VERSION_NAME/v/}" | grep -qP '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'; then
exit_on_error "The versionName '${RELEASE_VERSION_NAME/v/}' is not a valid version as per semantic version '2.0.0' spec in the format 'major.minor.patch(-prerelease)(+buildmetadata)'. https://semver.org/spec/v2.0.0.html."
fi
APK_DIR_PATH="./app/build/outputs/apk/debug"
APK_VERSION_TAG="$RELEASE_VERSION_NAME-github-debug" # Note the "-", GITHUB_SHA will already have "+" before it
APK_BASENAME_PREFIX="termux-app_$APK_VERSION_TAG"
# Used by attachment steps later
echo "APK_DIR_PATH=$APK_DIR_PATH" >> $GITHUB_ENV
echo "APK_VERSION_TAG=$APK_VERSION_TAG" >> $GITHUB_ENV
echo "APK_BASENAME_PREFIX=$APK_BASENAME_PREFIX" >> $GITHUB_ENV
echo "Building APKs for '$RELEASE_VERSION_NAME' build"
export TERMUX_APP_VERSION_NAME="${RELEASE_VERSION_NAME/v/}" # Used by app/build.gradle
export TERMUX_APK_VERSION_TAG="$APK_VERSION_TAG" # Used by app/build.gradle
if ! ./gradlew assembleDebug; then
exit_on_error "Build failed for '$RELEASE_VERSION_NAME' build."
fi
echo "Validating APKs"
for abi in universal arm64-v8a armeabi-v7a x86_64 x86; do
if ! test -f "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_$abi.apk"; then
files_found="$(ls "$APK_DIR_PATH")"
exit_on_error "Failed to find built APK at '$APK_DIR_PATH/${APK_BASENAME_PREFIX}_$abi.apk'. Files found: "$'\n'"$files_found"
fi
done
echo "Generating sha25sums file"
if ! (cd "$APK_DIR_PATH"; sha256sum \
"${APK_BASENAME_PREFIX}_universal.apk" \
"${APK_BASENAME_PREFIX}_arm64-v8a.apk" \
"${APK_BASENAME_PREFIX}_armeabi-v7a.apk" \
"${APK_BASENAME_PREFIX}_x86_64.apk" \
"${APK_BASENAME_PREFIX}_x86.apk" \
> sha256sums); then
exit_on_error "Generate sha25sums failed for '$RELEASE_VERSION_NAME' release."
fi
- name: Attach universal APK file
uses: actions/upload-artifact@v4
with:
name: ${{ env.APK_BASENAME_PREFIX }}_universal
path: |
${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_universal.apk
${{ env.APK_DIR_PATH }}/output-metadata.json
- name: Attach arm64-v8a APK file
uses: actions/upload-artifact@v4
with:
name: ${{ env.APK_BASENAME_PREFIX }}_arm64-v8a
path: |
${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_arm64-v8a.apk
${{ env.APK_DIR_PATH }}/output-metadata.json
- name: Attach armeabi-v7a APK file
uses: actions/upload-artifact@v4
with:
name: ${{ env.APK_BASENAME_PREFIX }}_armeabi-v7a
path: |
${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_armeabi-v7a.apk
${{ env.APK_DIR_PATH }}/output-metadata.json
- name: Attach x86_64 APK file
uses: actions/upload-artifact@v4
with:
name: ${{ env.APK_BASENAME_PREFIX }}_x86_64
path: |
${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_x86_64.apk
${{ env.APK_DIR_PATH }}/output-metadata.json
- name: Attach x86 APK file
uses: actions/upload-artifact@v4
with:
name: ${{ env.APK_BASENAME_PREFIX }}_x86
path: |
${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_x86.apk
${{ env.APK_DIR_PATH }}/output-metadata.json
- name: Attach sha256sums file
uses: actions/upload-artifact@v4
with:
name: sha256sums
path: |
${{ env.APK_DIR_PATH }}/sha256sums
${{ env.APK_DIR_PATH }}/output-metadata.json

View File

@@ -15,5 +15,5 @@ jobs:
name: "Validation" name: "Validation"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: gradle/wrapper-validation-action@v1 - uses: gradle/wrapper-validation-action@v1

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Clone repository - name: Clone repository
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Execute tests - name: Execute tests
run: | run: |
./gradlew test ./gradlew test

View File

@@ -0,0 +1,21 @@
name: Trigger Termux Library Builds on Jitpack
on:
release:
types:
- published
jobs:
trigger-termux-library-builds:
runs-on: ubuntu-latest
steps:
- name: Set vars
run: echo "TERMUX_LIB_VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_ENV # Do not include "v" prefix
- name: Echo release
run: echo "Triggering termux library builds on jitpack for '$TERMUX_LIB_VERSION' release after waiting for 3 mins"
- name: Trigger termux library builds on jitpack
run: |
sleep 180 # It will take some time for the new tag to be detected by Jitpack
curl --max-time 600 --no-progress-meter "https://jitpack.io/com/termux/termux-app/terminal-emulator/$TERMUX_LIB_VERSION/terminal-emulator-$TERMUX_LIB_VERSION.pom"
curl --max-time 600 --no-progress-meter "https://jitpack.io/com/termux/termux-app/terminal-view/$TERMUX_LIB_VERSION/terminal-view-$TERMUX_LIB_VERSION.pom"
curl --max-time 600 --no-progress-meter "https://jitpack.io/com/termux/termux-app/termux-shared/$TERMUX_LIB_VERSION/termux-shared-$TERMUX_LIB_VERSION.pom"

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@
# Built application files # Built application files
build/ build/
release/
*.apk *.apk
*.so *.so
.externalNativeBuild .externalNativeBuild

157
README.md
View File

@@ -3,6 +3,9 @@
[![Build status](https://github.com/termux/termux-app/workflows/Build/badge.svg)](https://github.com/termux/termux-app/actions) [![Build status](https://github.com/termux/termux-app/workflows/Build/badge.svg)](https://github.com/termux/termux-app/actions)
[![Testing status](https://github.com/termux/termux-app/workflows/Unit%20tests/badge.svg)](https://github.com/termux/termux-app/actions) [![Testing status](https://github.com/termux/termux-app/workflows/Unit%20tests/badge.svg)](https://github.com/termux/termux-app/actions)
[![Join the chat at https://gitter.im/termux/termux](https://badges.gitter.im/termux/termux.svg)](https://gitter.im/termux/termux) [![Join the chat at https://gitter.im/termux/termux](https://badges.gitter.im/termux/termux.svg)](https://gitter.im/termux/termux)
[![Join the Termux discord server](https://img.shields.io/discord/641256914684084234.svg?label=&logo=discord&logoColor=ffffff&color=5865F2)](https://discord.gg/HXpF69X)
[![Termux library releases at Jitpack](https://jitpack.io/v/termux/termux-app.svg)](https://jitpack.io/#termux/termux-app)
[Termux](https://termux.com) is an Android terminal application and Linux environment. [Termux](https://termux.com) is an Android terminal application and Linux environment.
@@ -12,7 +15,7 @@ Quick how-to about Termux package management is available at [Package Management
*** ***
**@termux is looking for Termux Application maintainers for implementing new features, fixing bugs and reviewing pull requests since current one (@fornwall) is inactive.** **@termux is looking for Termux Application maintainers for implementing new features, fixing bugs and reviewing pull requests since the current one (@fornwall) is inactive.**
Issue https://github.com/termux/termux-app/issues/1072 needs extra attention. Issue https://github.com/termux/termux-app/issues/1072 needs extra attention.
@@ -23,7 +26,9 @@ Issue https://github.com/termux/termux-app/issues/1072 needs extra attention.
- [Installation](#Installation) - [Installation](#Installation)
- [Uninstallation](#Uninstallation) - [Uninstallation](#Uninstallation)
- [Important Links](#Important-Links) - [Important Links](#Important-Links)
- [For Devs and Contributors](#For-Devs-and-Contributors) - [Debugging](#Debugging)
- [For Maintainers and Contributors](#For-Maintainers-and-Contributors)
- [Forking](#Forking)
## ##
@@ -44,38 +49,87 @@ The core [Termux](https://github.com/termux/termux-app) app comes with the follo
## Installation ## Installation
Latest version is `v0.118.0`.
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). 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).
The APK files of different sources are signed with different signature keys. The `Termux` app and all its plugins use the same [sharedUserId](https://developer.android.com/guide/topics/manifest/manifest-element) `com.termux` and so all their APKs installed on a device must have been signed with the same signature key to work together and so they must all be installed from the same source. Do not attempt to mix them together, i.e do not try to install an app or plugin from F-Droid and another one from a different source. Android Package Manager will also normally not allow installation of APKs with a different signatures and you will get errors on installation like `App not installed`, `Failed to install due to an unknown error`, `INSTALL_FAILED_UPDATE_INCOMPATIBLE`, `INSTALL_FAILED_SHARED_USER_INCOMPATIBLE`, `signatures do not match previously installed version`, etc. This restriction can be bypassed with root or with custom roms. The APK files of different sources are signed with different signature keys. The `Termux` app and all its plugins use the same [`sharedUserId`](https://developer.android.com/guide/topics/manifest/manifest-element) `com.termux` and so all their APKs installed on a device must have been signed with the same signature key to work together and so they must all be installed from the same source. Do not attempt to mix them together, i.e do not try to install an app or plugin from `F-Droid` and another one from a different source like `Github`. Android Package Manager will also normally not allow installation of APKs with different signatures and you will get errors on installation like `App not installed`, `Failed to install due to an unknown error`, `INSTALL_FAILED_UPDATE_INCOMPATIBLE`, `INSTALL_FAILED_SHARED_USER_INCOMPATIBLE`, `signatures do not match previously installed version`, etc. This restriction can be bypassed with root or with custom roms.
If you wish to install from a different source, then you must uninstall **any and all existing Termux or its plugin app APKs** from your device first, then install all new APKs from the same new source. Check [Uninstallation](#Uninstallation) section for details. You may also want to consider [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux) before uninstallation. If you wish to install from a different source, then you must **uninstall any and all existing Termux or its plugin app APKs** from your device first, then install all new APKs from the same new source. Check [Uninstallation](#Uninstallation) section for details. You may also want to consider [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux) before the uninstallation so that you can restore it after re-installing from Termux different source.
In the following paragraphs, *"bootstrap"* refers to the minimal packages that are shipped with the `termux-app` itself to start a working shell environment. Its zips are built and released [here](https://github.com/termux/termux-packages/releases).
### F-Droid ### F-Droid
Termux application can be obtained from F-Droid [here](https://f-droid.org/en/packages/com.termux/). It usually takes a few days (or even a week or more) for updates to be available on F-Droid once an update has been released on Github. F-Droid releases are built and published by F-Droid once they detect a new Github release. The Termux maintainers **do not** have any control over building and publishing of Termux app on F-Droid. Moreover, the Termux maintainers also do not have access to the APK signing keys of F-Droid releases, so we cannot release an APK ourselves on Github that would be compatible with F-Droid releases. Termux application can be obtained from `F-Droid` from [here](https://f-droid.org/en/packages/com.termux/).
### Debug Builds You **do not** need to download the `F-Droid` app (via the `Download F-Droid` link) to install Termux. You can download the Termux APK directly from the site by clicking the `Download APK` link at the bottom of each version section.
For users who don't want to wait for F-Droid releases and want to try out the latest features immediately or want to test their pull requests can get the APKs from [Github Actions](https://github.com/termux/termux-app/actions) page from the workflow runs labeled `Build`. The APK will be listed under `Artifacts` section. These are published for each commit done to the repository. These APKs are [debuggable](https://developer.android.com/studio/debug) and are also not compatible with other sources. It usually takes a few days (or even a week or more) for updates to be available on `F-Droid` once an update has been released on `Github`. The `F-Droid` releases are built and published by `F-Droid` once they [detect](https://gitlab.com/fdroid/fdroiddata/-/blob/master/metadata/com.termux.yml) a new `Github` release. The Termux maintainers **do not** have any control over the building and publishing of the Termux apps on `F-Droid`. Moreover, the Termux maintainers also do not have access to the APK signing keys of `F-Droid` releases, so we cannot release an APK ourselves on `Github` that would be compatible with `F-Droid` releases.
### Google Playstore **(Deprecated)** The `F-Droid` app often may not notify you of updates and you will manually have to do a pull down swipe action in the `Updates` tab of the app for it to check updates. Make sure battery optimizations are disabled for the app, check https://dontkillmyapp.com/ for details on how to do that.
**Termux and its plugins are no longer updated on [Google playstore](https://play.google.com/store/apps/details?id=com.termux) due to [android 10 issues](https://github.com/termux/termux-packages/wiki/Termux-and-Android-10).** The last version released for Android `>= 7` was `v0.101`. There are currently no immediate plans to resume updates on Google playstore. **It is highly recommended to not install Termux from playstore for now.** Any current users **should switch** to a different source like F-Droid. Only a universal APK is released, which will work on all supported architectures. The APK and bootstrap installation size will be `~180MB`. `F-Droid` does [not support](https://github.com/termux/termux-app/pull/1904) architecture specific APKs.
### Github
Termux application can be obtained on `Github` either from [`Github Releases`](https://github.com/termux/termux-app/releases) for version `>= 0.118.0` or from [`Github Build`](https://github.com/termux/termux-app/actions/workflows/debug_build.yml) action workflows.
The APKs for `Github Releases` will be listed under `Assets` drop-down of a release. These are automatically attached when a new version is released.
The APKs for `Github Build` action workflows will be listed under `Artifacts` section of a workflow run. These are created for each commit/push done to the repository and can be used by users who don't want to wait for releases and want to try out the latest features immediately or want to test their pull requests. Note that for action workflows, you need to be [**logged into a `Github` account**](https://github.com/login) for the `Artifacts` links to be enabled/clickable. If you are using the [`Github` app](https://github.com/mobile), then make sure to open workflow link in a browser like Chrome or Firefox that has your Github account logged in since the in-app browser may not be logged in.
The APKs for both of these are [`debuggable`](https://developer.android.com/studio/debug) and are compatible with each other but they are not compatible with other sources.
Both universal and architecture specific APKs are released. The APK and bootstrap installation size will be `~180MB` if using universal and `~120MB` if using architecture specific. Check [here](https://github.com/termux/termux-app/issues/2153) for details.
### Google Play Store **(Deprecated)**
**Termux and its plugins are no longer updated on [Google Play Store](https://play.google.com/store/apps/details?id=com.termux) due to [android 10 issues](https://github.com/termux/termux-packages/wiki/Termux-and-Android-10) and have been deprecated.** The last version released for Android `>= 7` was `v0.101`. **It is highly recommended to not install Termux apps from Play Store any more.**
There are plans for **unpublishing** the Termux app and all its plugins on Play Store soon so that new users cannot install it and for **disabling** the Termux apps with updates so that existing users **cannot continue using outdated versions**. You are encouraged to move to `F-Droid` or `Github` builds as soon as possible.
You **will not need to buy plugins again** if you bought them on Play Store. All plugins are free on `F-Droid` and `Github`.
You can backup all your data under `$HOME/` and `$PREFIX/` before changing installation source, and then restore it afterwards, by following instructions at [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux) before the uninstallation.
There is currently no work being done to solve android `10` issues and *working* updates will not be resumed on Google Play Store any time soon. We will continue targeting sdk `28` for now. So there is not much point in staying on Play Store builds and waiting for updates to be resumed. If for some reason you don't want to move to `F-Droid` or `Github` sources for now, then at least check [Package Management](https://github.com/termux/termux-packages/wiki/Package-Management) to **change your mirror**, otherwise, you will get **`repository is under maintenance or down`** errors when running `apt` or `pkg` commands. After that, it is also **highly advisable** to run `pkg upgrade` command to update all packages to the latest available versions, or at least update `termux-tools` package with `pkg install termux-tools` command.
Note that by upgrading old packages to latest versions, like that of `python` may break your setups/scripts since they may not be compatible anymore. Moreover, you will not be able to downgrade the package versions since termux repos only keep the latest version and you will have to manually rebuild the old versions of the packages if required as per https://github.com/termux/termux-packages/wiki/Building-packages.
If you plan on staying on Play Store sources in future as well, then you may want to **disable automatic updates in Play Store** for Termux apps, since if and when updates to disable Termux apps are released, then **you will not be able to downgrade** and **will be forced** to move since apps won't work anymore. Only a way to backup `termux-app` data may be provided. The `termux-tools` [version `>= 0.135`](https://github.com/termux/termux-packages/pull/7493) will also show a banner at the top of the terminal saying `You are likely using a very old version of Termux, probably installed from the Google Play Store.`, you can remove it by running `rm -f /data/data/com.termux/files/usr/etc/motd-playstore` and restarting the app.
#### Why Disable?
<details>
<summary></summary>
- They should be disabled because deprecated things get removed and are not supported after some time, its the standard practice. It has been many months now since deprecation was announced and updates have not been released on Play Store since after `29 September 2020`.
- The new versions have lots of **new features and fixes** which you can mostly check out in the Changelog of [`Github Releases`](https://github.com/termux/termux-app/releases) that you may be missing out. Extra detail is usually provided in [commit messages](https://github.com/termux/termux-app/commits/master).
- Users on old versions are quite often reporting issues in multiple repositories and support forums that were **fixed months ago**, which we then have to deal with. The maintainers of @termux work in their free time, majorly for free, to work on development and provide support and having to re-re-deal with old issues takes away the already limited time from current work and is not possible to continue doing. Play Store page of `termux-app` has been filled with bad reviews of *"broken app"*, even though its clearly mentioned on the page that app is not being updated, yet users don't read and still install and report issues.
- Asking people to pay for plugins when the `termux-app` at installation time is broken due to repository issues and has bugs is unethical.
- Old versions don't have proper logging/debugging and crash report support. Reporting bugs without logs or detailed info is not helpful in solving them.
- It's also easier for us to solve package related issues and provide custom functionality with app updates, which can't be done if users continue using old versions. For example, the [bintray shudown](https://github.com/termux/termux-packages/wiki/Package-Management) causing package install/update failures for new Play Store users is/was not an issue for F-Droid users since it is being shipped with updated bootstrap and repo info, hence no reported issues from new F-Droid users.
</details>
If for some reason you don't want to switch, then at least check [Package Management](https://github.com/termux/termux-packages/wiki/Package-Management) to **change your mirror**, otherwise you will get **`repository is under maintenance or down`** errors when running `apt` or `pkg` commands. After that, it is also **highly advisable** to run `pkg upgrade` command to update all packages to the latest available versions, or at least update `termux-tools` package with `pkg install termux-tools` command.
## ##
## Uninstallation ## Uninstallation
Uninstallation may be required if a user doesn't want Termux installed in their device anymore or is switching to a different [install source](#Installation). You may also want to consider [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux) before uninstallation. Uninstallation may be required if a user doesn't want Termux installed in their device anymore or is switching to a different [install source](#Installation). You may also want to consider [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux) before the uninstallation.
To uninstall Termux completely, you must uninstall **any and all existing Termux or its plugin app APKs** listed in [Termux App and Plugins](#Termux-App-and-Plugins). To uninstall Termux completely, you must uninstall **any and all existing Termux or its plugin app APKs** listed in [Termux App and Plugins](#Termux-App-and-Plugins).
Go to `Android Settings` -> `Applications` and then look for those apps. You can also use the search feature if its available on your device and search `termux` in the applications list. Go to `Android Settings` -> `Applications` and then look for those apps. You can also use the search feature if its available on your device and search `termux` in the applications list.
Even if you think you have not installed any of the plugins, its strongly suggesting to go through the application list in Android settings and double check. Even if you think you have not installed any of the plugins, it's strongly suggested to go through the application list in Android settings and double-check.
## ##
@@ -90,8 +144,8 @@ The main ones are the following.
- [Termux Reddit community](https://reddit.com/r/termux) - [Termux Reddit community](https://reddit.com/r/termux)
- [Termux Matrix Channel](https://matrix.to/#termux_termux:gitter.im) - [Termux Matrix Channel](https://matrix.to/#termux_termux:gitter.im)
- [Termux Dev Matrix Channel](https://matrix.to/#termux_dev:gitter.im) - [Termux Dev Matrix Channel](https://matrix.to/#termux_dev:gitter.im)
- [Termux Twitter](http://twitter.com/termux/) - [Termux Twitter](https://twitter.com/termux/)
- [Termux Reports Email](mailto:termuxreports@groups.io) - [Termux Reports Email](mailto:support@termux.dev)
### Wikis ### Wikis
@@ -104,42 +158,99 @@ The main ones are the following.
- [Termux File System Layout](https://github.com/termux/termux-packages/wiki/Termux-file-system-layout) - [Termux File System Layout](https://github.com/termux/termux-packages/wiki/Termux-file-system-layout)
- [Differences From Linux](https://wiki.termux.com/wiki/Differences_from_Linux) - [Differences From Linux](https://wiki.termux.com/wiki/Differences_from_Linux)
- [Package Management](https://wiki.termux.com/wiki/Package_Management) - [Package Management](https://wiki.termux.com/wiki/Package_Management)
- [Remote_Access](https://wiki.termux.com/wiki/Remote_Access) - [Remote Access](https://wiki.termux.com/wiki/Remote_Access)
- [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux) - [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux)
- [Terminal Settings](https://wiki.termux.com/wiki/Terminal_Settings) - [Terminal Settings](https://wiki.termux.com/wiki/Terminal_Settings)
- [Touch Keyboard](https://wiki.termux.com/wiki/Touch_Keyboard) - [Touch Keyboard](https://wiki.termux.com/wiki/Touch_Keyboard)
- [Android Storage and Sharing Data with Other Apps](https://wiki.termux.com/wiki/Internal_and_external_storage) - [Android Storage and Sharing Data with Other Apps](https://wiki.termux.com/wiki/Internal_and_external_storage)
- [Android APIs](https://wiki.termux.com/wiki/Termux:API) - [Android APIs](https://wiki.termux.com/wiki/Termux:API)
- [Moved Termux Packages Hosting From Bintray to IPFS](https://github.com/termux/termux-packages/issues/6348) - [Moved Termux Packages Hosting From Bintray to IPFS](https://github.com/termux/termux-packages/issues/6348)
- [Running Commands in Termux From Other Apps via `RUN_COMMAND` intent](https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent)
- [Termux and Android 10](https://github.com/termux/termux-packages/wiki/Termux-and-Android-10) - [Termux and Android 10](https://github.com/termux/termux-packages/wiki/Termux-and-Android-10)
### Terminal
<details>
<summary></summary>
### Terminal resources ### Terminal resources
- [XTerm control sequences](http://invisible-island.net/xterm/ctlseqs/ctlseqs.html) - [XTerm control sequences](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
- [vt100.net](http://vt100.net/) - [vt100.net](https://vt100.net/)
- [Terminal codes (ANSI and terminfo equivalents)](http://wiki.bash-hackers.org/scripting/terminalcodes) - [Terminal codes (ANSI and terminfo equivalents)](https://wiki.bash-hackers.org/scripting/terminalcodes)
### Terminal emulators ### Terminal emulators
- VTE (libvte): Terminal emulator widget for GTK+, mainly used in gnome-terminal. [Source](https://github.com/GNOME/vte), [Open Issues](https://bugzilla.gnome.org/buglist.cgi?quicksearch=product%3A%22vte%22+), and [All (including closed) issues](https://bugzilla.gnome.org/buglist.cgi?bug_status=RESOLVED&bug_status=VERIFIED&chfield=resolution&chfieldfrom=-2000d&chfieldvalue=FIXED&product=vte&resolution=FIXED). - VTE (libvte): Terminal emulator widget for GTK+, mainly used in gnome-terminal. [Source](https://github.com/GNOME/vte), [Open Issues](https://bugzilla.gnome.org/buglist.cgi?quicksearch=product%3A%22vte%22+), and [All (including closed) issues](https://bugzilla.gnome.org/buglist.cgi?bug_status=RESOLVED&bug_status=VERIFIED&chfield=resolution&chfieldfrom=-2000d&chfieldvalue=FIXED&product=vte&resolution=FIXED).
- iTerm 2: OS X terminal application. [Source](https://github.com/gnachman/iTerm2), [Issues](https://gitlab.com/gnachman/iterm2/issues) and [Documentation](http://www.iterm2.com/documentation.html) (which includes [iTerm2 proprietary escape codes](http://www.iterm2.com/documentation-escape-codes.html)). - iTerm 2: OS X terminal application. [Source](https://github.com/gnachman/iTerm2), [Issues](https://gitlab.com/gnachman/iterm2/issues) and [Documentation](https://iterm2.com/documentation.html) (which includes [iTerm2 proprietary escape codes](https://iterm2.com/documentation-escape-codes.html)).
- Konsole: KDE terminal application. [Source](https://projects.kde.org/projects/kde/applications/konsole/repository), in particular [tests](https://projects.kde.org/projects/kde/applications/konsole/repository/revisions/master/show/tests), [Bugs](https://bugs.kde.org/buglist.cgi?bug_severity=critical&bug_severity=grave&bug_severity=major&bug_severity=crash&bug_severity=normal&bug_severity=minor&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole) and [Wishes](https://bugs.kde.org/buglist.cgi?bug_severity=wishlist&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole). - Konsole: KDE terminal application. [Source](https://projects.kde.org/projects/kde/applications/konsole/repository), in particular [tests](https://projects.kde.org/projects/kde/applications/konsole/repository/revisions/master/show/tests), [Bugs](https://bugs.kde.org/buglist.cgi?bug_severity=critical&bug_severity=grave&bug_severity=major&bug_severity=crash&bug_severity=normal&bug_severity=minor&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole) and [Wishes](https://bugs.kde.org/buglist.cgi?bug_severity=wishlist&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole).
- hterm: JavaScript terminal implementation from Chromium. [Source](https://github.com/chromium/hterm), including [tests](https://github.com/chromium/hterm/blob/master/js/hterm_vt_tests.js), and [Google group](https://groups.google.com/a/chromium.org/forum/#!forum/chromium-hterm). - hterm: JavaScript terminal implementation from Chromium. [Source](https://github.com/chromium/hterm), including [tests](https://github.com/chromium/hterm/blob/master/js/hterm_vt_tests.js), and [Google group](https://groups.google.com/a/chromium.org/forum/#!forum/chromium-hterm).
- xterm: The grandfather of terminal emulators. [Source](http://invisible-island.net/datafiles/release/xterm.tar.gz). - xterm: The grandfather of terminal emulators. [Source](https://invisible-island.net/datafiles/release/xterm.tar.gz).
- Connectbot: Android SSH client. [Source](https://github.com/connectbot/connectbot) - Connectbot: Android SSH client. [Source](https://github.com/connectbot/connectbot)
- Android Terminal Emulator: Android terminal app which Termux terminal handling is based on. Inactive. [Source](https://github.com/jackpal/Android-Terminal-Emulator). - Android Terminal Emulator: Android terminal app which Termux terminal handling is based on. Inactive. [Source](https://github.com/jackpal/Android-Terminal-Emulator).
</details>
## ##
## For Devs and Contributors ### Debugging
The [termux-shared](termux-shared) library was added in [`v0.109`](https://github.com/termux/termux-app/releases/tag/v0.109). It defines shared constants and utils of Termux app and its plugins. It was created to allow for removal of all hardcoded paths in Termux app. The termux plugins will hopefully use this in future as well. If you are contributing code that is using a constant or a util that may be shared, then define it in `termux-shared` library if it currently doesn't exist and reference it from there. Update the relevant changelogs as well. Pull requests using hardcoded values **will/should not** be accepted. You can help debug problems of the `Termux` app and its plugins by setting appropriate `logcat` `Log Level` in `Termux` app settings -> `<APP_NAME>` -> `Debugging` -> `Log Level` (Requires `Termux` app version `>= 0.118.0`). The `Log Level` defaults to `Normal` and log level `Verbose` currently logs additional information. Its best to revert log level to `Normal` after you have finished debugging since private data may otherwise be passed to `logcat` during normal operation and moreover, additional logging increases execution time.
The plugin apps **do not execute the commands themselves** but send execution intents to `Termux` app, which has its own log level which can be set in `Termux` app settings -> `Termux` -> `Debugging` -> `Log Level`. So you must set log level for both `Termux` and the respective plugin app settings to get all the info.
Once log levels have been set, you can run the `logcat` command in `Termux` app terminal to view the logs in realtime (`Ctrl+c` to stop) or use `logcat -d > logcat.txt` to take a dump of the log. You can also view the logs from a PC over `ADB`. For more information, check official android `logcat` guide [here](https://developer.android.com/studio/command-line/logcat).
Moreover, users can generate termux files `stat` info and `logcat` dump automatically too with terminal's long hold options menu `More` -> `Report Issue` option and selecting `YES` in the prompt shown to add debug info. This can be helpful for reporting and debugging other issues. If the report generated is too large, then `Save To File` option in context menu (3 dots on top right) of `ReportActivity` can be used and the file viewed/shared instead.
Users must post complete report (optionally without sensitive info) when reporting issues. Issues opened with **(partial) screenshots of error reports** instead of text will likely be automatically closed/deleted.
##### Log Levels
- `Off` - Log nothing.
- `Normal` - Start logging error, warn and info messages and stacktraces.
- `Debug` - Start logging debug messages.
- `Verbose` - Start logging verbose messages.
##
## For Maintainers and Contributors
The [termux-shared](termux-shared) library was added in [`v0.109`](https://github.com/termux/termux-app/releases/tag/v0.109). It defines shared constants and utils of the Termux app and its plugins. It was created to allow for the removal of all hardcoded paths in the Termux app. Some of the termux plugins are using this as well and rest will in future. If you are contributing code that is using a constant or a util that may be shared, then define it in `termux-shared` library if it currently doesn't exist and reference it from there. Update the relevant changelogs as well. Pull requests using hardcoded values **will/should not** be accepted. Termux app and plugin specific classes must be added under `com.termux.shared.termux` package and general classes outside it. The [`termux-shared` `LICENSE`](termux-shared/LICENSE.md) must also be checked and updated if necessary when contributing code. The licenses of any external library or code must be honoured.
The main Termux constants are defined by [`TermuxConstants`](https://github.com/termux/termux-app/blob/master/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java) class. It also contains information on how to fork Termux or build it with your own package name. Changing the package name will require building the bootstrap zip packages and other packages with the new `$PREFIX`, check [Building Packages](https://github.com/termux/termux-packages/wiki/Building-packages) for more info. The main Termux constants are defined by [`TermuxConstants`](https://github.com/termux/termux-app/blob/master/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java) class. It also contains information on how to fork Termux or build it with your own package name. Changing the package name will require building the bootstrap zip packages and other packages with the new `$PREFIX`, check [Building Packages](https://github.com/termux/termux-packages/wiki/Building-packages) for more info.
Check [Termux Libraries](https://github.com/termux/termux-app/wiki/Termux-Libraries) for how to import termux libraries in plugin apps and [Forking and Local Development](https://github.com/termux/termux-app/wiki/Termux-Libraries#forking-and-local-development) for how to update termux libraries for plugins.
Commit messages **must** use [Conventional Commits](https://www.conventionalcommits.org) specs so that chagelogs can automatically be generated by the [`create-conventional-changelog`](https://github.com/termux/create-conventional-changelog) script, check its repo for further details on the spec. Use the following `types` as `Added: Add foo`, `Added|Fixed: Add foo and fix bar`, `Changed!: Change baz as a breaking change`, etc. You can optionally add a scope as well, like `Fixed(terminal): Some bug`. The space after `:` is necessary.
- **Added** for new features.
- **Changed** for changes in existing functionality.
- **Deprecated** for soon-to-be removed features.
- **Removed** for now removed features.
- **Fixed** for any bug fixes.
- **Security** in case of vulnerabilities.
- **Docs** for updating documentation.
Changelogs for releases are generated based on [Keep a Changelog](https://github.com/olivierlacan/keep-a-changelog) specs.
The `versionName` in `build.gradle` files of Termux and its plugin apps must follow the [semantic version `2.0.0` spec](https://semver.org/spec/v2.0.0.html) in the format `major.minor.patch(-prerelease)(+buildmetadata)`. When bumping `versionName` in `build.gradle` files and when creating a tag for new releases on github, make sure to include the patch number as well, like `v0.1.0` instead of just `v0.1`. The `build.gradle` files and `attach_debug_apks_to_release` workflow validates the version as well and the build/attachment will fail if `versionName` does not follow the spec.
##
## Forking
- Check [`TermuxConstants`](https://github.com/termux/termux-app/blob/master/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java) javadocs for instructions on what changes to make in the app to change package name.
- You also need to recompile bootstrap zip for the new package name. Check [here](https://github.com/termux/termux-app/issues/1983) and [here](https://github.com/termux/termux-app/issues/2081#issuecomment-865280111) for experimental work on it.
- Currently, not all plugins use `TermuxConstants` from `termux-shared` library and have hardcoded `com.termux` values and will need to be manually patched.
- If forking termux plugins, check [Forking and Local Development](https://github.com/termux/termux-app/wiki/Termux-Libraries#forking-and-local-development) for info on how to use termux libraries for plugins.

View File

@@ -1,14 +1,18 @@
plugins { plugins {
id "com.android.application" id "com.android.application"
} }
android { android {
compileSdkVersion project.properties.compileSdkVersion.toInteger() compileSdkVersion project.properties.compileSdkVersion.toInteger()
ndkVersion project.properties.ndkVersion ndkVersion = System.getenv("JITPACK_NDK_VERSION") ?: project.properties.ndkVersion
def appVersionName = System.getenv("TERMUX_APP_VERSION_NAME") ?: ""
def apkVersionTag = System.getenv("TERMUX_APK_VERSION_TAG") ?: ""
def splitAPKsForDebugBuilds = System.getenv("TERMUX_SPLIT_APKS_FOR_DEBUG_BUILDS") ?: "1"
def splitAPKsForReleaseBuilds = System.getenv("TERMUX_SPLIT_APKS_FOR_RELEASE_BUILDS") ?: "0" // F-Droid does not support split APKs #1904
dependencies { dependencies {
implementation "androidx.annotation:annotation:1.2.0" implementation "androidx.annotation:annotation:1.3.0"
implementation "androidx.core:core:1.6.0-rc01" implementation "androidx.core:core:1.6.0"
implementation "androidx.drawerlayout:drawerlayout:1.1.1" implementation "androidx.drawerlayout:drawerlayout:1.1.1"
implementation "androidx.preference:preference:1.1.1" implementation "androidx.preference:preference:1.1.1"
implementation "androidx.viewpager:viewpager:1.0.0" implementation "androidx.viewpager:viewpager:1.0.0"
@@ -26,8 +30,11 @@ android {
applicationId "com.termux" applicationId "com.termux"
minSdkVersion project.properties.minSdkVersion.toInteger() minSdkVersion project.properties.minSdkVersion.toInteger()
targetSdkVersion project.properties.targetSdkVersion.toInteger() targetSdkVersion project.properties.targetSdkVersion.toInteger()
versionCode 115 versionCode 1000
versionName "0.115" versionName "0.118.1"
if (appVersionName) versionName = appVersionName
validateVersionName(versionName)
manifestPlaceholders.TERMUX_PACKAGE_NAME = "com.termux" manifestPlaceholders.TERMUX_PACKAGE_NAME = "com.termux"
manifestPlaceholders.TERMUX_APP_NAME = "Termux" manifestPlaceholders.TERMUX_APP_NAME = "Termux"
@@ -44,10 +51,15 @@ android {
} }
} }
ndk { splits {
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' abi {
enable ((gradle.startParameter.taskNames.any { it.contains("Debug") } && splitAPKsForDebugBuilds == "1") ||
(gradle.startParameter.taskNames.any { it.contains("Release") } && splitAPKsForReleaseBuilds == "1"))
reset ()
include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
universalApk true
}
} }
} }
signingConfigs { signingConfigs {
@@ -62,7 +74,7 @@ android {
buildTypes { buildTypes {
release { release {
minifyEnabled true minifyEnabled true
shrinkResources true shrinkResources false // Reproducible builds
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
@@ -91,6 +103,25 @@ android {
includeAndroidResources = true includeAndroidResources = true
} }
} }
packagingOptions {
jniLibs {
useLegacyPackaging true
}
}
applicationVariants.all { variant ->
variant.outputs.all { output ->
if (variant.buildType.name == "debug") {
def abi = output.getFilter(com.android.build.OutputFile.ABI)
outputFileName = new File("termux-app_" + (apkVersionTag ? apkVersionTag : "debug") + "_" + (abi ? abi : "universal") + ".apk")
} else if (variant.buildType.name == "release") {
def abi = output.getFilter(com.android.build.OutputFile.ABI)
outputFileName = new File("termux-app_" + (apkVersionTag ? apkVersionTag : "release") + "_" + (abi ? abi : "universal") + ".apk")
}
}
}
} }
dependencies { dependencies {
@@ -99,9 +130,16 @@ dependencies {
} }
task versionName { task versionName {
doLast { doLast {
print android.defaultConfig.versionName print android.defaultConfig.versionName
} }
}
def validateVersionName(String versionName) {
// https://semver.org/spec/v2.0.0.html#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
// ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$
if (!java.util.regex.Pattern.matches("^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?\$", versionName))
throw new GradleException("The versionName '" + versionName + "' is not a valid version as per semantic version '2.0.0' spec in the format 'major.minor.patch(-prerelease)(+buildmetadata)'. https://semver.org/spec/v2.0.0.html.")
} }
def downloadBootstrap(String arch, String expectedChecksum, String version) { def downloadBootstrap(String arch, String expectedChecksum, String version) {
@@ -118,6 +156,7 @@ def downloadBootstrap(String arch, String expectedChecksum, String version) {
digest.update(buffer, 0, readBytes) digest.update(buffer, 0, readBytes)
} }
def checksum = new BigInteger(1, digest.digest()).toString(16) def checksum = new BigInteger(1, digest.digest()).toString(16)
while (checksum.length() < 64) { checksum = "0" + checksum }
if (checksum == expectedChecksum) { if (checksum == expectedChecksum) {
return return
} else { } else {
@@ -139,6 +178,7 @@ def downloadBootstrap(String arch, String expectedChecksum, String version) {
out.close() out.close()
def checksum = new BigInteger(1, digest.digest()).toString(16) def checksum = new BigInteger(1, digest.digest()).toString(16)
while (checksum.length() < 64) { checksum = "0" + checksum }
if (checksum != expectedChecksum) { if (checksum != expectedChecksum) {
file.delete() file.delete()
throw new GradleException("Wrong checksum for " + remoteUrl + ": expected: " + expectedChecksum + ", actual: " + checksum) throw new GradleException("Wrong checksum for " + remoteUrl + ": expected: " + expectedChecksum + ", actual: " + checksum)
@@ -155,16 +195,16 @@ clean {
task downloadBootstraps() { task downloadBootstraps() {
doLast { doLast {
def version = "2021.06.30-r1" def version = "2024.06.17-r1+apt-android-7"
downloadBootstrap("aarch64", "ce56ce9a4e8845bd1d35cc2695bbdd636c72625ee10ce21c9b98ab38ebbee5ab", version) downloadBootstrap("aarch64", "91a90661597fe14bb3c3563f5f65b243c0baaec42f2bc3d2243ff459e3942fb6", version)
downloadBootstrap("arm", "537e81951c7d3d3f3def9ce6778e1032457488e21edb2c037a1e0e680c39e747", version) downloadBootstrap("arm", "d54b5eb2a305d72f267f9704deaca721b2bebbd3d4cca134aec31da719707997", version)
downloadBootstrap("i686", "3c2ca858c0225671c00c44ac182e31819ffa93ec624e95e02824e7d6d30ca1b4", version) downloadBootstrap("i686", "06a51ac1c679d68d52045509f1a705622c8f41748ef753660e31e3b6a846eba2", version)
downloadBootstrap("x86_64", "93c50d36b45bca42bb014395e8e184e5b540adcad5d4e215f7e64ebf0d655d2b", version) downloadBootstrap("x86_64", "4c8e43474c8d9543e01d4cbf3c4d7f59bbe4d696c38f6dece2b6ab3ba8881f2e", version)
} }
} }
afterEvaluate { afterEvaluate {
android.applicationVariants.all { variant -> android.applicationVariants.all { variant ->
variant.javaCompileProvider.get().dependsOn(downloadBootstraps) variant.javaCompileProvider.get().dependsOn(downloadBootstraps)
} }
} }

View File

@@ -10,3 +10,8 @@
-dontobfuscate -dontobfuscate
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile
#-keepattributes SourceFile,LineNumberTable #-keepattributes SourceFile,LineNumberTable
# Temp fix for androidx.window:window:1.0.0-alpha09 imported by termux-shared
# https://issuetracker.google.com/issues/189001730
# https://android-review.googlesource.com/c/platform/frameworks/support/+/1757630
-keep class androidx.window.** { *; }

View File

@@ -32,6 +32,8 @@
<uses-permission android:name="android.permission.DUMP" /> <uses-permission android:name="android.permission.DUMP" />
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" tools:ignore="ProtectedPermissions" />
<uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
<application <application
android:name=".app.TermuxApplication" android:name=".app.TermuxApplication"
@@ -44,14 +46,6 @@
android:supportsRtl="false" android:supportsRtl="false"
android:theme="@style/Theme.Termux"> android:theme="@style/Theme.Termux">
<!--
This (or rather, value 2.1 or higher) is needed to make the Samsung Galaxy S8
mark the app with "This app is optimized to run in full screen."
-->
<meta-data
android:name="android.max_aspect"
android:value="10.0" />
<activity <activity
android:name=".app.TermuxActivity" android:name=".app.TermuxActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|uiMode|keyboard|keyboardHidden|navigation" android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|uiMode|keyboard|keyboardHidden|navigation"
@@ -143,6 +137,7 @@
</intent-filter> </intent-filter>
</activity> </activity>
<provider <provider
android:name=".filepicker.TermuxDocumentsProvider" android:name=".filepicker.TermuxDocumentsProvider"
android:authorities="${TERMUX_PACKAGE_NAME}.documents" android:authorities="${TERMUX_PACKAGE_NAME}.documents"
@@ -154,9 +149,23 @@
</intent-filter> </intent-filter>
</provider> </provider>
<provider
android:name=".app.TermuxOpenReceiver$ContentProvider"
android:authorities="${TERMUX_PACKAGE_NAME}.files"
android:exported="true"
android:grantUriPermissions="true"
android:permission="${TERMUX_PACKAGE_NAME}.permission.RUN_COMMAND" />
<receiver android:name=".app.TermuxOpenReceiver" android:exported="false" />
<receiver android:name=".shared.activities.ReportActivity$ReportActivityBroadcastReceiver" android:exported="false" />
<service <service
android:name=".app.TermuxService" android:name=".app.TermuxService"
android:exported="false" /> android:exported="false" />
<service <service
android:name=".app.RunCommandService" android:name=".app.RunCommandService"
android:exported="true" android:exported="true"
@@ -166,21 +175,30 @@
</intent-filter> </intent-filter>
</service> </service>
<receiver android:name=".app.TermuxOpenReceiver" />
<provider <!-- This (or rather, value 2.1 or higher) is needed to make the Samsung Galaxy S8 mark the
android:name=".app.TermuxOpenReceiver$ContentProvider" app with "This app is optimized to run in full screen." -->
android:authorities="${TERMUX_PACKAGE_NAME}.files" <meta-data
android:exported="true" android:name="android.max_aspect"
android:grantUriPermissions="true" android:value="10.0" />
android:readPermission="android.permission.permRead" />
<!-- https://developer.samsung.com/samsung-dex/modify-optimizing.html -->
<!-- Version < 3.0. DeX Mode and Screen Mirroring support -->
<meta-data
android:name="com.samsung.android.keepalive.density"
android:value="true" />
<!-- Version >= 3.0. DeX Dual Mode support -->
<meta-data
android:name="com.samsung.android.multidisplay.keep_process_alive"
android:value="true" />
<meta-data <meta-data
android:name="com.sec.android.support.multiwindow" android:name="com.sec.android.support.multiwindow"
android:value="true" /> android:value="true" />
<meta-data
android:name="com.samsung.android.multidisplay.keep_process_alive"
android:value="true" />
</application> </application>
</manifest> </manifest>

View File

@@ -13,6 +13,7 @@ import com.termux.R;
import com.termux.shared.data.DataUtils; import com.termux.shared.data.DataUtils;
import com.termux.shared.data.IntentUtils; import com.termux.shared.data.IntentUtils;
import com.termux.shared.file.TermuxFileUtils; import com.termux.shared.file.TermuxFileUtils;
import com.termux.shared.file.filesystem.FileType;
import com.termux.shared.models.errors.Errno; import com.termux.shared.models.errors.Errno;
import com.termux.shared.models.errors.Error; import com.termux.shared.models.errors.Error;
import com.termux.shared.termux.TermuxConstants; import com.termux.shared.termux.TermuxConstants;
@@ -72,11 +73,10 @@ public class RunCommandService extends Service {
errmsg = this.getString(R.string.error_run_command_service_invalid_intent_action, intent.getAction()); errmsg = this.getString(R.string.error_run_command_service_invalid_intent_action, intent.getAction());
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg); executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return Service.START_NOT_STICKY; return stopService();
} }
executionCommand.executable = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH, null); String executableExtra = executionCommand.executable = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH, null);
executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS, null); executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS, null);
/* /*
@@ -101,6 +101,7 @@ public class RunCommandService extends Service {
executionCommand.stdin = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_STDIN, null); executionCommand.stdin = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_STDIN, null);
executionCommand.workingDirectory = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_WORKDIR, null); executionCommand.workingDirectory = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_WORKDIR, null);
executionCommand.inBackground = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false); executionCommand.inBackground = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false);
executionCommand.backgroundCustomLogLevel = IntentUtils.getIntegerExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL, null);
executionCommand.sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION); executionCommand.sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION);
executionCommand.commandLabel = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL, "RUN_COMMAND Execution Intent Command"); executionCommand.commandLabel = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL, "RUN_COMMAND Execution Intent Command");
executionCommand.commandDescription = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION, null); executionCommand.commandDescription = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION, null);
@@ -121,11 +122,11 @@ public class RunCommandService extends Service {
// user knows someone tried to run a command in termux context, since it may be malicious // user knows someone tried to run a command in termux context, since it may be malicious
// app or imported (tasker) plugin project and not the user himself. If a pending intent is // app or imported (tasker) plugin project and not the user himself. If a pending intent is
// also sent, then its creator is also logged and shown. // also sent, then its creator is also logged and shown.
errmsg = PluginUtils.checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(this); errmsg = PluginUtils.checkIfAllowExternalAppsPolicyIsViolated(this, LOG_TAG);
if (errmsg != null) { if (errmsg != null) {
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg); executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true); PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true);
return Service.START_NOT_STICKY; return stopService();
} }
@@ -135,7 +136,7 @@ public class RunCommandService extends Service {
errmsg = this.getString(R.string.error_run_command_service_mandatory_extra_missing, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH); errmsg = this.getString(R.string.error_run_command_service_mandatory_extra_missing, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg); executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return Service.START_NOT_STICKY; return stopService();
} }
// Get canonical path of executable // Get canonical path of executable
@@ -147,10 +148,9 @@ public class RunCommandService extends Service {
FileUtils.APP_EXECUTABLE_FILE_PERMISSIONS, true, true, FileUtils.APP_EXECUTABLE_FILE_PERMISSIONS, true, true,
false); false);
if (error != null) { if (error != null) {
error.appendMessage("\n" + this.getString(R.string.msg_executable_absolute_path, executionCommand.executable));
executionCommand.setStateFailed(error); executionCommand.setStateFailed(error);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return Service.START_NOT_STICKY; return stopService();
} }
@@ -169,18 +169,25 @@ public class RunCommandService extends Service {
true, true, true, true, true, true,
false, true); false, true);
if (error != null) { if (error != null) {
error.appendMessage("\n" + this.getString(R.string.msg_working_directory_absolute_path, executionCommand.workingDirectory));
executionCommand.setStateFailed(error); executionCommand.setStateFailed(error);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return Service.START_NOT_STICKY; return stopService();
} }
} }
// If the executable passed as the extra was an applet for coreutils/busybox, then we must
// use it instead of the canonical path above since otherwise arguments would be passed to
// coreutils/busybox instead and command would fail. Broken symlinks would already have been
// validated so it should be fine to use it.
executableExtra = TermuxFileUtils.getExpandedTermuxPath(executableExtra);
if (FileUtils.getFileType(executableExtra, false) == FileType.SYMLINK) {
Logger.logVerbose(LOG_TAG, "The executableExtra path \"" + executableExtra + "\" is a symlink so using it instead of the canonical path \"" + executionCommand.executable + "\"");
executionCommand.executable = executableExtra;
}
executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(executionCommand.executable).build();
executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(TermuxFileUtils.getExpandedTermuxPath(executionCommand.executable)).build(); Logger.logVerboseExtended(LOG_TAG, executionCommand.toString());
Logger.logVerbose(LOG_TAG, executionCommand.toString());
// Create execution intent with the action TERMUX_SERVICE#ACTION_SERVICE_EXECUTE to be sent to the TERMUX_SERVICE // Create execution intent with the action TERMUX_SERVICE#ACTION_SERVICE_EXECUTE to be sent to the TERMUX_SERVICE
Intent execIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, executionCommand.executableUri); Intent execIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, executionCommand.executableUri);
@@ -189,6 +196,7 @@ public class RunCommandService extends Service {
execIntent.putExtra(TERMUX_SERVICE.EXTRA_STDIN, executionCommand.stdin); execIntent.putExtra(TERMUX_SERVICE.EXTRA_STDIN, executionCommand.stdin);
if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) execIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, executionCommand.workingDirectory); if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) execIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, executionCommand.workingDirectory);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, executionCommand.inBackground); execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, executionCommand.inBackground);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL, DataUtils.getStringFromInteger(executionCommand.backgroundCustomLogLevel, null));
execIntent.putExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION, executionCommand.sessionAction); execIntent.putExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION, executionCommand.sessionAction);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_LABEL, executionCommand.commandLabel); execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_LABEL, executionCommand.commandLabel);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, executionCommand.commandDescription); execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, executionCommand.commandDescription);
@@ -211,8 +219,11 @@ public class RunCommandService extends Service {
this.startService(execIntent); this.startService(execIntent);
} }
runStopForeground(); return stopService();
}
private int stopService() {
runStopForeground();
return Service.START_NOT_STICKY; return Service.START_NOT_STICKY;
} }
@@ -234,7 +245,7 @@ public class RunCommandService extends Service {
Notification.Builder builder = NotificationUtils.geNotificationBuilder(this, Notification.Builder builder = NotificationUtils.geNotificationBuilder(this,
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_LOW, TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_LOW,
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_NAME, null, null, TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_NAME, null, null,
null, NotificationUtils.NOTIFICATION_MODE_SILENT); null, null, NotificationUtils.NOTIFICATION_MODE_SILENT);
if (builder == null) return null; if (builder == null) return null;
// No need to show a timestamp: // No need to show a timestamp:

View File

@@ -29,10 +29,12 @@ import android.view.autofill.AutofillManager;
import android.widget.EditText; import android.widget.EditText;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.ListView; import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.Toast; import android.widget.Toast;
import com.termux.R; import com.termux.R;
import com.termux.app.terminal.TermuxActivityRootView; import com.termux.app.terminal.TermuxActivityRootView;
import com.termux.shared.activities.ReportActivity;
import com.termux.shared.packages.PermissionUtils; import com.termux.shared.packages.PermissionUtils;
import com.termux.shared.termux.TermuxConstants; import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY; import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
@@ -43,11 +45,12 @@ import com.termux.app.terminal.TermuxSessionsListViewController;
import com.termux.app.terminal.io.TerminalToolbarViewPager; import com.termux.app.terminal.io.TerminalToolbarViewPager;
import com.termux.app.terminal.TermuxTerminalSessionClient; import com.termux.app.terminal.TermuxTerminalSessionClient;
import com.termux.app.terminal.TermuxTerminalViewClient; import com.termux.app.terminal.TermuxTerminalViewClient;
import com.termux.app.terminal.io.extrakeys.ExtraKeysView; import com.termux.shared.terminal.io.extrakeys.ExtraKeysView;
import com.termux.app.settings.properties.TermuxAppSharedProperties; import com.termux.app.settings.properties.TermuxAppSharedProperties;
import com.termux.shared.interact.TextInputDialogUtils; import com.termux.shared.interact.TextInputDialogUtils;
import com.termux.shared.logger.Logger; import com.termux.shared.logger.Logger;
import com.termux.shared.termux.TermuxUtils; import com.termux.shared.termux.TermuxUtils;
import com.termux.shared.view.ViewUtils;
import com.termux.terminal.TerminalSession; import com.termux.terminal.TerminalSession;
import com.termux.terminal.TerminalSessionClient; import com.termux.terminal.TerminalSessionClient;
import com.termux.app.utils.CrashUtils; import com.termux.app.utils.CrashUtils;
@@ -182,6 +185,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
// notification with the crash details if it did // notification with the crash details if it did
CrashUtils.notifyAppCrashOnLastRun(this, LOG_TAG); CrashUtils.notifyAppCrashOnLastRun(this, LOG_TAG);
// Delete ReportInfo serialized object files from cache older than 14 days
ReportActivity.deleteReportInfoFilesOlderThanXDays(this, 14, false);
// Load termux shared properties // Load termux shared properties
mProperties = new TermuxAppSharedProperties(this); mProperties = new TermuxAppSharedProperties(this);
@@ -200,6 +206,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
return; return;
} }
setMargins();
mTermuxActivityRootView = findViewById(R.id.activity_termux_root_view); mTermuxActivityRootView = findViewById(R.id.activity_termux_root_view);
mTermuxActivityRootView.setActivity(this); mTermuxActivityRootView.setActivity(this);
mTermuxActivityBottomSpaceView = findViewById(R.id.activity_termux_bottom_space_view); mTermuxActivityBottomSpaceView = findViewById(R.id.activity_termux_bottom_space_view);
@@ -259,7 +267,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
if (mTermuxTerminalViewClient != null) if (mTermuxTerminalViewClient != null)
mTermuxTerminalViewClient.onStart(); mTermuxTerminalViewClient.onStart();
if (!mProperties.isTerminalMarginAdjustmentDisabled()) if (mPreferences.isTerminalMarginAdjustmentEnabled())
addTermuxActivityRootViewGlobalLayoutListener(); addTermuxActivityRootViewGlobalLayoutListener();
registerTermuxActivityBroadcastReceiver(); registerTermuxActivityBroadcastReceiver();
@@ -357,7 +365,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
Bundle bundle = getIntent().getExtras(); Bundle bundle = getIntent().getExtras();
boolean launchFailsafe = false; boolean launchFailsafe = false;
if (bundle != null) { if (bundle != null) {
launchFailsafe = bundle.getBoolean(TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION, false); launchFailsafe = bundle.getBoolean(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false);
} }
mTermuxTerminalSessionClient.addNewSession(launchFailsafe, null); mTermuxTerminalSessionClient.addNewSession(launchFailsafe, null);
} catch (WindowManager.BadTokenException e) { } catch (WindowManager.BadTokenException e) {
@@ -372,7 +380,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
Intent i = getIntent(); Intent i = getIntent();
if (i != null && Intent.ACTION_RUN.equals(i.getAction())) { if (i != null && Intent.ACTION_RUN.equals(i.getAction())) {
// Android 7.1 app shortcut from res/xml/shortcuts.xml. // Android 7.1 app shortcut from res/xml/shortcuts.xml.
boolean isFailSafe = i.getBooleanExtra(TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION, false); boolean isFailSafe = i.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false);
mTermuxTerminalSessionClient.addNewSession(isFailSafe, null); mTermuxTerminalSessionClient.addNewSession(isFailSafe, null);
} else { } else {
mTermuxTerminalSessionClient.setCurrentSession(mTermuxTerminalSessionClient.getCurrentStoredSessionOrLast()); mTermuxTerminalSessionClient.setCurrentSession(mTermuxTerminalSessionClient.getCurrentStoredSessionOrLast());
@@ -412,6 +420,13 @@ public final class TermuxActivity extends Activity implements ServiceConnection
} }
} }
private void setMargins() {
RelativeLayout relativeLayout = findViewById(R.id.activity_termux_root_relative_layout);
int marginHorizontal = mProperties.getTerminalMarginHorizontal();
int marginVertical = mProperties.getTerminalMarginVertical();
ViewUtils.setLayoutMarginsInDp(relativeLayout, marginHorizontal, marginVertical, marginHorizontal, marginVertical);
}
public void addTermuxActivityRootViewGlobalLayoutListener() { public void addTermuxActivityRootViewGlobalLayoutListener() {
@@ -452,7 +467,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
private void setTerminalToolbarView(Bundle savedInstanceState) { private void setTerminalToolbarView(Bundle savedInstanceState) {
final ViewPager terminalToolbarViewPager = findViewById(R.id.terminal_toolbar_view_pager); final ViewPager terminalToolbarViewPager = getTerminalToolbarViewPager();
if (mPreferences.shouldShowTerminalToolbar()) terminalToolbarViewPager.setVisibility(View.VISIBLE); if (mPreferences.shouldShowTerminalToolbar()) terminalToolbarViewPager.setVisibility(View.VISIBLE);
ViewGroup.LayoutParams layoutParams = terminalToolbarViewPager.getLayoutParams(); ViewGroup.LayoutParams layoutParams = terminalToolbarViewPager.getLayoutParams();
@@ -469,8 +484,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
} }
private void setTerminalToolbarHeight() { private void setTerminalToolbarHeight() {
final ViewPager terminalToolbarViewPager = findViewById(R.id.terminal_toolbar_view_pager); final ViewPager terminalToolbarViewPager = getTerminalToolbarViewPager();
if (terminalToolbarViewPager == null) return; if (terminalToolbarViewPager == null) return;
ViewGroup.LayoutParams layoutParams = terminalToolbarViewPager.getLayoutParams(); ViewGroup.LayoutParams layoutParams = terminalToolbarViewPager.getLayoutParams();
layoutParams.height = (int) Math.round(mTerminalToolbarDefaultHeight * layoutParams.height = (int) Math.round(mTerminalToolbarDefaultHeight *
(mProperties.getExtraKeysInfo() == null ? 0 : mProperties.getExtraKeysInfo().getMatrix().length) * (mProperties.getExtraKeysInfo() == null ? 0 : mProperties.getExtraKeysInfo().getMatrix().length) *
@@ -479,13 +495,13 @@ public final class TermuxActivity extends Activity implements ServiceConnection
} }
public void toggleTerminalToolbar() { public void toggleTerminalToolbar() {
final ViewPager terminalToolbarViewPager = findViewById(R.id.terminal_toolbar_view_pager); final ViewPager terminalToolbarViewPager = getTerminalToolbarViewPager();
if (terminalToolbarViewPager == null) return; if (terminalToolbarViewPager == null) return;
final boolean showNow = mPreferences.toogleShowTerminalToolbar(); final boolean showNow = mPreferences.toogleShowTerminalToolbar();
Logger.showToast(this, (showNow ? getString(R.string.msg_enabling_terminal_toolbar) : getString(R.string.msg_disabling_terminal_toolbar)), true); Logger.showToast(this, (showNow ? getString(R.string.msg_enabling_terminal_toolbar) : getString(R.string.msg_disabling_terminal_toolbar)), true);
terminalToolbarViewPager.setVisibility(showNow ? View.VISIBLE : View.GONE); terminalToolbarViewPager.setVisibility(showNow ? View.VISIBLE : View.GONE);
if (showNow && terminalToolbarViewPager.getCurrentItem() == 1) { if (showNow && isTerminalToolbarTextInputViewSelected()) {
// Focus the text input view if just revealed. // Focus the text input view if just revealed.
findViewById(R.id.terminal_toolbar_text_input).requestFocus(); findViewById(R.id.terminal_toolbar_text_input).requestFocus();
} }
@@ -744,6 +760,20 @@ public final class TermuxActivity extends Activity implements ServiceConnection
return (DrawerLayout) findViewById(R.id.drawer_layout); return (DrawerLayout) findViewById(R.id.drawer_layout);
} }
public ViewPager getTerminalToolbarViewPager() {
return (ViewPager) findViewById(R.id.terminal_toolbar_view_pager);
}
public boolean isTerminalViewSelected() {
return getTerminalToolbarViewPager().getCurrentItem() == 0;
}
public boolean isTerminalToolbarTextInputViewSelected() {
return getTerminalToolbarViewPager().getCurrentItem() == 1;
}
public void termuxSessionListNotifyUpdated() { public void termuxSessionListNotifyUpdated() {
mTermuxSessionListViewController.notifyDataSetChanged(); mTermuxSessionListViewController.notifyDataSetChanged();
} }
@@ -850,10 +880,12 @@ public final class TermuxActivity extends Activity implements ServiceConnection
mProperties.loadTermuxPropertiesFromDisk(); mProperties.loadTermuxPropertiesFromDisk();
if (mExtraKeysView != null) { if (mExtraKeysView != null) {
mExtraKeysView.setButtonTextAllCaps(mProperties.shouldExtraKeysTextBeAllCaps());
mExtraKeysView.reload(mProperties.getExtraKeysInfo()); mExtraKeysView.reload(mProperties.getExtraKeysInfo());
} }
} }
setMargins();
setTerminalToolbarHeight(); setTerminalToolbarHeight();
if (mTermuxTerminalSessionClient != null) if (mTermuxTerminalSessionClient != null)

View File

@@ -5,7 +5,6 @@ import android.app.AlertDialog;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.Context; import android.content.Context;
import android.os.Environment; import android.os.Environment;
import android.os.UserManager;
import android.system.Os; import android.system.Os;
import android.util.Pair; import android.util.Pair;
import android.view.WindowManager; import android.view.WindowManager;
@@ -13,10 +12,17 @@ import android.view.WindowManager;
import com.termux.R; import com.termux.R;
import com.termux.app.utils.CrashUtils; import com.termux.app.utils.CrashUtils;
import com.termux.shared.file.FileUtils; import com.termux.shared.file.FileUtils;
import com.termux.shared.file.TermuxFileUtils;
import com.termux.shared.interact.MessageDialogUtils; import com.termux.shared.interact.MessageDialogUtils;
import com.termux.shared.logger.Logger; import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.models.ExecutionCommand;
import com.termux.shared.models.errors.Error; import com.termux.shared.models.errors.Error;
import com.termux.shared.packages.PackageUtils;
import com.termux.shared.shell.TermuxShellEnvironmentClient;
import com.termux.shared.shell.TermuxTask;
import com.termux.shared.termux.TermuxConstants; import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxUtils;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
@@ -28,6 +34,11 @@ import java.util.List;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream; import java.util.zip.ZipInputStream;
import static com.termux.shared.termux.TermuxConstants.TERMUX_PREFIX_DIR;
import static com.termux.shared.termux.TermuxConstants.TERMUX_PREFIX_DIR_PATH;
import static com.termux.shared.termux.TermuxConstants.TERMUX_STAGING_PREFIX_DIR;
import static com.termux.shared.termux.TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH;
/** /**
* Install the Termux bootstrap packages if necessary by following the below steps: * Install the Termux bootstrap packages if necessary by following the below steps:
* <p/> * <p/>
@@ -53,34 +64,49 @@ final class TermuxInstaller {
/** Performs bootstrap setup if necessary. */ /** Performs bootstrap setup if necessary. */
static void setupBootstrapIfNeeded(final Activity activity, final Runnable whenDone) { static void setupBootstrapIfNeeded(final Activity activity, final Runnable whenDone) {
String bootstrapErrorMessage;
Error filesDirectoryAccessibleError;
// This will also call Context.getFilesDir(), which should ensure that termux files directory
// is created if it does not already exist
filesDirectoryAccessibleError = TermuxFileUtils.isTermuxFilesDirectoryAccessible(activity, true, true);
boolean isFilesDirectoryAccessible = filesDirectoryAccessibleError == null;
// Termux can only be run as the primary user (device owner) since only that // Termux can only be run as the primary user (device owner) since only that
// account has the expected file system paths. Verify that: // account has the expected file system paths. Verify that:
UserManager um = (UserManager) activity.getSystemService(Context.USER_SERVICE); if (!PackageUtils.isCurrentUserThePrimaryUser(activity)) {
boolean isPrimaryUser = um.getSerialNumberForUser(android.os.Process.myUserHandle()) == 0; bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message, MarkdownUtils.getMarkdownCodeForString(TERMUX_PREFIX_DIR_PATH, false));
if (!isPrimaryUser) { Logger.logError(LOG_TAG, "isFilesDirectoryAccessible: " + isFilesDirectoryAccessible);
String bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message, TermuxConstants.TERMUX_PREFIX_DIR_PATH);
Logger.logError(LOG_TAG, bootstrapErrorMessage); Logger.logError(LOG_TAG, bootstrapErrorMessage);
sendBootstrapCrashReportNotification(activity, bootstrapErrorMessage);
MessageDialogUtils.exitAppWithErrorMessage(activity, MessageDialogUtils.exitAppWithErrorMessage(activity,
activity.getString(R.string.bootstrap_error_title), activity.getString(R.string.bootstrap_error_title),
bootstrapErrorMessage); bootstrapErrorMessage);
return; return;
} }
final String PREFIX_FILE_PATH = TermuxConstants.TERMUX_PREFIX_DIR_PATH; if (!isFilesDirectoryAccessible) {
final File PREFIX_FILE = TermuxConstants.TERMUX_PREFIX_DIR; bootstrapErrorMessage = Error.getMinimalErrorString(filesDirectoryAccessibleError) + "\nTERMUX_FILES_DIR: " + MarkdownUtils.getMarkdownCodeForString(TermuxConstants.TERMUX_FILES_DIR_PATH, false);
Logger.logError(LOG_TAG, bootstrapErrorMessage);
sendBootstrapCrashReportNotification(activity, bootstrapErrorMessage);
MessageDialogUtils.showMessage(activity,
activity.getString(R.string.bootstrap_error_title),
bootstrapErrorMessage, null);
return;
}
// If prefix directory exists, even if its a symlink to a valid directory and symlink is not broken/dangling // If prefix directory exists, even if its a symlink to a valid directory and symlink is not broken/dangling
if (FileUtils.directoryFileExists(PREFIX_FILE_PATH, true)) { if (FileUtils.directoryFileExists(TERMUX_PREFIX_DIR_PATH, true)) {
File[] PREFIX_FILE_LIST = PREFIX_FILE.listFiles(); File[] PREFIX_FILE_LIST = TERMUX_PREFIX_DIR.listFiles();
// If prefix directory is empty or only contains the tmp directory // If prefix directory is empty or only contains the tmp directory
if(PREFIX_FILE_LIST == null || PREFIX_FILE_LIST.length == 0 || (PREFIX_FILE_LIST.length == 1 && TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH.equals(PREFIX_FILE_LIST[0].getAbsolutePath()))) { if(PREFIX_FILE_LIST == null || PREFIX_FILE_LIST.length == 0 || (PREFIX_FILE_LIST.length == 1 && TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH.equals(PREFIX_FILE_LIST[0].getAbsolutePath()))) {
Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" exists but is empty or only contains the tmp directory."); Logger.logInfo(LOG_TAG, "The termux prefix directory \"" + TERMUX_PREFIX_DIR_PATH + "\" exists but is empty or only contains the tmp directory.");
} else { } else {
whenDone.run(); whenDone.run();
return; return;
} }
} else if (FileUtils.fileExists(PREFIX_FILE_PATH, false)) { } else if (FileUtils.fileExists(TERMUX_PREFIX_DIR_PATH, false)) {
Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" does not exist but another file exists at its destination."); Logger.logInfo(LOG_TAG, "The termux prefix directory \"" + TERMUX_PREFIX_DIR_PATH + "\" does not exist but another file exists at its destination.");
} }
final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false); final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false);
@@ -92,24 +118,35 @@ final class TermuxInstaller {
Error error; Error error;
final String STAGING_PREFIX_PATH = TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH;
final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH);
// Delete prefix staging directory or any file at its destination // Delete prefix staging directory or any file at its destination
error = FileUtils.deleteFile("prefix staging directory", STAGING_PREFIX_PATH, true); error = FileUtils.deleteFile("termux prefix staging directory", TERMUX_STAGING_PREFIX_DIR_PATH, true);
if (error != null) { if (error != null) {
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error)); showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
return; return;
} }
// Delete prefix directory or any file at its destination // Delete prefix directory or any file at its destination
error = FileUtils.deleteFile("prefix directory", PREFIX_FILE_PATH, true); error = FileUtils.deleteFile("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true);
if (error != null) { if (error != null) {
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error)); showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
return; return;
} }
Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + STAGING_PREFIX_PATH + "\"."); // Create prefix staging directory if it does not already exist and set required permissions
error = TermuxFileUtils.isTermuxPrefixStagingDirectoryAccessible(true, true);
if (error != null) {
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
return;
}
// Create prefix directory if it does not already exist and set required permissions
error = TermuxFileUtils.isTermuxPrefixDirectoryAccessible(true, true);
if (error != null) {
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
return;
}
Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + TERMUX_STAGING_PREFIX_DIR_PATH + "\".");
final byte[] buffer = new byte[8096]; final byte[] buffer = new byte[8096];
final List<Pair<String, String>> symlinks = new ArrayList<>(50); final List<Pair<String, String>> symlinks = new ArrayList<>(50);
@@ -126,23 +163,23 @@ final class TermuxInstaller {
if (parts.length != 2) if (parts.length != 2)
throw new RuntimeException("Malformed symlink line: " + line); throw new RuntimeException("Malformed symlink line: " + line);
String oldPath = parts[0]; String oldPath = parts[0];
String newPath = STAGING_PREFIX_PATH + "/" + parts[1]; String newPath = TERMUX_STAGING_PREFIX_DIR_PATH + "/" + parts[1];
symlinks.add(Pair.create(oldPath, newPath)); symlinks.add(Pair.create(oldPath, newPath));
error = ensureDirectoryExists(new File(newPath).getParentFile()); error = ensureDirectoryExists(new File(newPath).getParentFile());
if (error != null) { if (error != null) {
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error)); showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
return; return;
} }
} }
} else { } else {
String zipEntryName = zipEntry.getName(); String zipEntryName = zipEntry.getName();
File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName); File targetFile = new File(TERMUX_STAGING_PREFIX_DIR_PATH, zipEntryName);
boolean isDirectory = zipEntry.isDirectory(); boolean isDirectory = zipEntry.isDirectory();
error = ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile()); error = ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile());
if (error != null) { if (error != null) {
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error)); showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
return; return;
} }
@@ -152,7 +189,9 @@ final class TermuxInstaller {
while ((readBytes = zipInput.read(buffer)) != -1) while ((readBytes = zipInput.read(buffer)) != -1)
outStream.write(buffer, 0, readBytes); outStream.write(buffer, 0, readBytes);
} }
if (zipEntryName.startsWith("bin/") || zipEntryName.startsWith("libexec") || zipEntryName.startsWith("lib/apt/methods")) { if (zipEntryName.startsWith("bin/") || zipEntryName.startsWith("libexec") ||
zipEntryName.startsWith("lib/apt/apt-helper") || zipEntryName.startsWith("lib/apt/methods") ||
zipEntryName.equals("etc/termux/bootstrap/termux-bootstrap-second-stage.sh")) {
//noinspection OctalInteger //noinspection OctalInteger
Os.chmod(targetFile.getAbsolutePath(), 0700); Os.chmod(targetFile.getAbsolutePath(), 0700);
} }
@@ -167,17 +206,39 @@ final class TermuxInstaller {
Os.symlink(symlink.first, symlink.second); Os.symlink(symlink.first, symlink.second);
} }
Logger.logInfo(LOG_TAG, "Moving prefix staging to prefix directory."); Logger.logInfo(LOG_TAG, "Moving termux prefix staging to prefix directory.");
if (!STAGING_PREFIX_FILE.renameTo(PREFIX_FILE)) { if (!TERMUX_STAGING_PREFIX_DIR.renameTo(TERMUX_PREFIX_DIR)) {
throw new RuntimeException("Moving prefix staging to prefix directory failed"); 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.");
String termuxBootstrapSecondStageFile = TERMUX_PREFIX_DIR_PATH + "/etc/termux/bootstrap/termux-bootstrap-second-stage.sh";
if (FileUtils.fileExists(termuxBootstrapSecondStageFile, false)) {
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
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;
}
} }
Logger.logInfo(LOG_TAG, "Bootstrap packages installed successfully."); Logger.logInfo(LOG_TAG, "Bootstrap packages installed successfully.");
activity.runOnUiThread(whenDone); activity.runOnUiThread(whenDone);
} catch (final Exception e) { } catch (final Exception e) {
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e))); showBootstrapErrorDialog(activity, whenDone, Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)));
} finally { } finally {
activity.runOnUiThread(() -> { activity.runOnUiThread(() -> {
@@ -192,11 +253,11 @@ final class TermuxInstaller {
}.start(); }.start();
} }
public static void showBootstrapErrorDialog(Activity activity, String PREFIX_FILE_PATH, Runnable whenDone, String message) { public static void showBootstrapErrorDialog(Activity activity, Runnable whenDone, String message) {
Logger.logErrorExtended(LOG_TAG, "Bootstrap Error:\n" + message); Logger.logErrorExtended(LOG_TAG, "Bootstrap Error:\n" + message);
// Send a notification with the exception so that the user knows why bootstrap setup failed // Send a notification with the exception so that the user knows why bootstrap setup failed
CrashUtils.sendCrashReportNotification(activity, LOG_TAG, "## Bootstrap Error\n\n" + message, true); sendBootstrapCrashReportNotification(activity, message);
activity.runOnUiThread(() -> { activity.runOnUiThread(() -> {
try { try {
@@ -207,7 +268,7 @@ final class TermuxInstaller {
}) })
.setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> { .setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
dialog.dismiss(); dialog.dismiss();
FileUtils.deleteFile("prefix directory", PREFIX_FILE_PATH, true); FileUtils.deleteFile("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true);
TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone); TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone);
}).show(); }).show();
} catch (WindowManager.BadTokenException e1) { } catch (WindowManager.BadTokenException e1) {
@@ -216,6 +277,13 @@ final class TermuxInstaller {
}); });
} }
private static void sendBootstrapCrashReportNotification(Activity activity, String message) {
CrashUtils.sendCrashReportNotification(activity, LOG_TAG,
"## Bootstrap Error\n\n" + message + "\n\n" +
TermuxUtils.getTermuxDebugMarkdownString(activity),
true, true);
}
static void setupStorageSymlinks(final Context context) { static void setupStorageSymlinks(final Context context) {
final String LOG_TAG = "termux-storage"; final String LOG_TAG = "termux-storage";
@@ -231,7 +299,7 @@ final class TermuxInstaller {
if (error != null) { if (error != null) {
Logger.logErrorAndShowToast(context, LOG_TAG, error.getMessage()); Logger.logErrorAndShowToast(context, LOG_TAG, error.getMessage());
Logger.logErrorExtended(LOG_TAG, "Setup Storage Error\n" + error.toString()); Logger.logErrorExtended(LOG_TAG, "Setup Storage Error\n" + error.toString());
CrashUtils.sendCrashReportNotification(context, LOG_TAG, "## Setup Storage Error\n\n" + Error.getErrorMarkdownString(error), true); CrashUtils.sendCrashReportNotification(context, LOG_TAG, "## Setup Storage Error\n\n" + Error.getErrorMarkdownString(error), true, true);
return; return;
} }
@@ -270,7 +338,7 @@ final class TermuxInstaller {
} catch (Exception e) { } catch (Exception e) {
Logger.logErrorAndShowToast(context, LOG_TAG, e.getMessage()); Logger.logErrorAndShowToast(context, LOG_TAG, e.getMessage());
Logger.logStackTraceWithMessage(LOG_TAG, "Setup Storage Error: Error setting up link", e); Logger.logStackTraceWithMessage(LOG_TAG, "Setup Storage Error: Error setting up link", e);
CrashUtils.sendCrashReportNotification(context, LOG_TAG, "## Setup Storage Error\n\n" + Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)), true); CrashUtils.sendCrashReportNotification(context, LOG_TAG, "## Setup Storage Error\n\n" + Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)), true, true);
} }
} }
}.start(); }.start();

View File

@@ -13,6 +13,8 @@ import android.os.ParcelFileDescriptor;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.webkit.MimeTypeMap; import android.webkit.MimeTypeMap;
import com.termux.app.utils.PluginUtils;
import com.termux.shared.data.IntentUtils;
import com.termux.shared.logger.Logger; import com.termux.shared.logger.Logger;
import com.termux.shared.termux.TermuxConstants; import com.termux.shared.termux.TermuxConstants;
@@ -34,6 +36,8 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
return; return;
} }
Logger.logVerbose(LOG_TAG, "Intent Received:\n" + IntentUtils.getIntentString(intent));
final String filePath = data.getPath(); final String filePath = data.getPath();
final String contentTypeExtra = intent.getStringExtra("content-type"); final String contentTypeExtra = intent.getStringExtra("content-type");
final boolean useChooser = intent.getBooleanExtra("chooser", false); final boolean useChooser = intent.getBooleanExtra("chooser", false);
@@ -111,6 +115,8 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
public static class ContentProvider extends android.content.ContentProvider { public static class ContentProvider extends android.content.ContentProvider {
private static final String LOG_TAG = "TermuxContentProvider";
@Override @Override
public boolean onCreate() { public boolean onCreate() {
return true; return true;
@@ -178,15 +184,33 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
File file = new File(uri.getPath()); File file = new File(uri.getPath());
try { try {
String path = file.getCanonicalPath(); String path = file.getCanonicalPath();
Logger.logDebug(LOG_TAG, "Open file request received for \"" + path + "\" with mode \"" + mode + "\"");
String storagePath = Environment.getExternalStorageDirectory().getCanonicalPath(); String storagePath = Environment.getExternalStorageDirectory().getCanonicalPath();
// See https://support.google.com/faqs/answer/7496913: // See https://support.google.com/faqs/answer/7496913:
if (!(path.startsWith(TermuxConstants.TERMUX_FILES_DIR_PATH) || path.startsWith(storagePath))) { if (!(path.startsWith(TermuxConstants.TERMUX_FILES_DIR_PATH) || path.startsWith(storagePath))) {
throw new IllegalArgumentException("Invalid path: " + path); throw new IllegalArgumentException("Invalid path: " + path);
} }
// If "allow-external-apps" property to not set to "true", then throw exception
String errmsg = PluginUtils.checkIfAllowExternalAppsPolicyIsViolated(getContext(), LOG_TAG);
if (errmsg != null) {
throw new IllegalArgumentException(errmsg);
}
// Do not allow apps with RUN_COMMAND permission to modify termux apps properties files,
// including allow-external-apps
if (TermuxConstants.TERMUX_PROPERTIES_PRIMARY_FILE_PATH.equals(path) ||
TermuxConstants.TERMUX_PROPERTIES_SECONDARY_FILE_PATH.equals(path) ||
TermuxConstants.TERMUX_FLOAT_PROPERTIES_PRIMARY_FILE_PATH.equals(path) ||
TermuxConstants.TERMUX_FLOAT_PROPERTIES_SECONDARY_FILE_PATH.equals(path)) {
mode = "r";
}
} catch (IOException e) { } catch (IOException e) {
throw new IllegalArgumentException(e); throw new IllegalArgumentException(e);
} }
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode(mode));
} }
} }

View File

@@ -19,6 +19,8 @@ import android.os.PowerManager;
import android.provider.Settings; import android.provider.Settings;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import androidx.annotation.Nullable;
import com.termux.R; import com.termux.R;
import com.termux.app.settings.properties.TermuxAppSharedProperties; import com.termux.app.settings.properties.TermuxAppSharedProperties;
import com.termux.app.terminal.TermuxTerminalSessionClient; import com.termux.app.terminal.TermuxTerminalSessionClient;
@@ -47,8 +49,6 @@ import com.termux.terminal.TerminalSessionClient;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.annotation.Nullable;
/** /**
* A service holding a list of {@link TermuxSession} in {@link #mTermuxSessions} and background {@link TermuxTask} * A service holding a list of {@link TermuxSession} in {@link #mTermuxSessions} and background {@link TermuxTask}
* in {@link #mTermuxTasks}, showing a foreground notification while running so that it is not terminated. * in {@link #mTermuxTasks}, showing a foreground notification while running so that it is not terminated.
@@ -364,10 +364,11 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, TERMUX_SERVICE.EXTRA_ARGUMENTS, null); executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, TERMUX_SERVICE.EXTRA_ARGUMENTS, null);
if (executionCommand.inBackground) if (executionCommand.inBackground)
executionCommand.stdin = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_STDIN, null); executionCommand.stdin = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_STDIN, null);
executionCommand.backgroundCustomLogLevel = IntentUtils.getIntegerExtraIfSet(intent, TERMUX_SERVICE.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL, null);
} }
executionCommand.workingDirectory = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_WORKDIR, null); executionCommand.workingDirectory = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_WORKDIR, null);
executionCommand.isFailsafe = intent.getBooleanExtra(TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION, false); executionCommand.isFailsafe = intent.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false);
executionCommand.sessionAction = intent.getStringExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION); executionCommand.sessionAction = intent.getStringExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION);
executionCommand.commandLabel = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_LABEL, "Execution Intent Command"); executionCommand.commandLabel = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_LABEL, "Execution Intent Command");
executionCommand.commandDescription = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, null); executionCommand.commandDescription = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, null);
@@ -426,7 +427,7 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
} }
if (Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE) if (Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE)
Logger.logVerbose(LOG_TAG, executionCommand.toString()); Logger.logVerboseExtended(LOG_TAG, executionCommand.toString());
TermuxTask newTermuxTask = TermuxTask.execute(this, executionCommand, this, new TermuxShellEnvironmentClient(), false); TermuxTask newTermuxTask = TermuxTask.execute(this, executionCommand, this, new TermuxShellEnvironmentClient(), false);
if (newTermuxTask == null) { if (newTermuxTask == null) {
@@ -518,7 +519,7 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
} }
if (Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE) if (Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE)
Logger.logVerbose(LOG_TAG, executionCommand.toString()); Logger.logVerboseExtended(LOG_TAG, executionCommand.toString());
// If the execution command was started for a plugin, only then will the stdout be set // If the execution command was started for a plugin, only then will the stdout be set
// Otherwise if command was manually started by the user like by adding a new terminal session, // Otherwise if command was manually started by the user like by adding a new terminal session,
@@ -707,7 +708,7 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
// Set pending intent to be launched when notification is clicked // Set pending intent to be launched when notification is clicked
Intent notificationIntent = TermuxActivity.newInstance(this); Intent notificationIntent = TermuxActivity.newInstance(this);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0); PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
// Set notification text // Set notification text
@@ -731,8 +732,8 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
// Build the notification // Build the notification
Notification.Builder builder = NotificationUtils.geNotificationBuilder(this, Notification.Builder builder = NotificationUtils.geNotificationBuilder(this,
TermuxConstants.TERMUX_APP_NOTIFICATION_CHANNEL_ID, priority, TermuxConstants.TERMUX_APP_NOTIFICATION_CHANNEL_ID, priority,
getText(R.string.application_name), notificationText, null, TermuxConstants.TERMUX_APP_NAME, notificationText, null,
pendingIntent, NotificationUtils.NOTIFICATION_MODE_SILENT); contentIntent, null, NotificationUtils.NOTIFICATION_MODE_SILENT);
if (builder == null) return null; if (builder == null) return null;
// No need to show a timestamp: // No need to show a timestamp:

View File

@@ -2,6 +2,7 @@ package com.termux.app.activities;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
@@ -11,11 +12,15 @@ import androidx.preference.PreferenceFragmentCompat;
import com.termux.R; import com.termux.R;
import com.termux.shared.activities.ReportActivity; import com.termux.shared.activities.ReportActivity;
import com.termux.shared.file.FileUtils;
import com.termux.shared.models.ReportInfo; import com.termux.shared.models.ReportInfo;
import com.termux.app.models.UserAction; import com.termux.app.models.UserAction;
import com.termux.shared.interact.ShareUtils; import com.termux.shared.interact.ShareUtils;
import com.termux.shared.packages.PackageUtils; import com.termux.shared.packages.PackageUtils;
import com.termux.shared.settings.preferences.TermuxAPIAppSharedPreferences;
import com.termux.shared.settings.preferences.TermuxFloatAppSharedPreferences;
import com.termux.shared.settings.preferences.TermuxTaskerAppSharedPreferences; import com.termux.shared.settings.preferences.TermuxTaskerAppSharedPreferences;
import com.termux.shared.settings.preferences.TermuxWidgetAppSharedPreferences;
import com.termux.shared.termux.AndroidUtils; import com.termux.shared.termux.AndroidUtils;
import com.termux.shared.termux.TermuxConstants; import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxUtils; import com.termux.shared.termux.TermuxUtils;
@@ -53,17 +58,47 @@ public class SettingsActivity extends AppCompatActivity {
setPreferencesFromResource(R.xml.root_preferences, rootKey); setPreferencesFromResource(R.xml.root_preferences, rootKey);
configureTermuxAPIPreference(context);
configureTermuxFloatPreference(context);
configureTermuxTaskerPreference(context); configureTermuxTaskerPreference(context);
configureTermuxWidgetPreference(context);
configureAboutPreference(context); configureAboutPreference(context);
configureDonatePreference(context); configureDonatePreference(context);
} }
private void configureTermuxAPIPreference(@NonNull Context context) {
Preference termuxAPIPreference = findPreference("termux_api");
if (termuxAPIPreference != null) {
TermuxAPIAppSharedPreferences preferences = TermuxAPIAppSharedPreferences.build(context, false);
// If failed to get app preferences, then likely app is not installed, so do not show its preference
termuxAPIPreference.setVisible(preferences != null);
}
}
private void configureTermuxFloatPreference(@NonNull Context context) {
Preference termuxFloatPreference = findPreference("termux_float");
if (termuxFloatPreference != null) {
TermuxFloatAppSharedPreferences preferences = TermuxFloatAppSharedPreferences.build(context, false);
// If failed to get app preferences, then likely app is not installed, so do not show its preference
termuxFloatPreference.setVisible(preferences != null);
}
}
private void configureTermuxTaskerPreference(@NonNull Context context) { private void configureTermuxTaskerPreference(@NonNull Context context) {
Preference termuxTaskerPrefernce = findPreference("termux_tasker"); Preference termuxTaskerPreference = findPreference("termux_tasker");
if (termuxTaskerPrefernce != null) { if (termuxTaskerPreference != null) {
TermuxTaskerAppSharedPreferences preferences = TermuxTaskerAppSharedPreferences.build(context, false); TermuxTaskerAppSharedPreferences preferences = TermuxTaskerAppSharedPreferences.build(context, false);
// If failed to get app preferences, then likely app is not installed, so do not show its preference // If failed to get app preferences, then likely app is not installed, so do not show its preference
termuxTaskerPrefernce.setVisible(preferences != null); termuxTaskerPreference.setVisible(preferences != null);
}
}
private void configureTermuxWidgetPreference(@NonNull Context context) {
Preference termuxWidgetPreference = findPreference("termux_widget");
if (termuxWidgetPreference != null) {
TermuxWidgetAppSharedPreferences preferences = TermuxWidgetAppSharedPreferences.build(context, false);
// If failed to get app preferences, then likely app is not installed, so do not show its preference
termuxWidgetPreference.setVisible(preferences != null);
} }
} }
@@ -86,7 +121,13 @@ public class SettingsActivity extends AppCompatActivity {
aboutString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context)); aboutString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
aboutString.append("\n\n").append(TermuxUtils.getImportantLinksMarkdownString(context)); aboutString.append("\n\n").append(TermuxUtils.getImportantLinksMarkdownString(context));
ReportActivity.startReportActivity(context, new ReportInfo(UserAction.ABOUT.getName(), TermuxConstants.TERMUX_APP.TERMUX_SETTINGS_ACTIVITY_NAME, title, null, aboutString.toString(), null, false)); String userActionName = UserAction.ABOUT.getName();
ReportActivity.startReportActivity(context, new ReportInfo(userActionName,
TermuxConstants.TERMUX_APP.TERMUX_SETTINGS_ACTIVITY_NAME, title, null,
aboutString.toString(), null, false,
userActionName,
Environment.getExternalStorageDirectory() + "/" +
FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true)));
} }
}.start(); }.start();

View File

@@ -0,0 +1,49 @@
package com.termux.app.fragments.settings;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Keep;
import androidx.preference.PreferenceDataStore;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import com.termux.R;
import com.termux.shared.settings.preferences.TermuxAPIAppSharedPreferences;
@Keep
public class TermuxAPIPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(TermuxAPIPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.termux_api_preferences, rootKey);
}
}
class TermuxAPIPreferencesDataStore extends PreferenceDataStore {
private final Context mContext;
private final TermuxAPIAppSharedPreferences mPreferences;
private static TermuxAPIPreferencesDataStore mInstance;
private TermuxAPIPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxAPIAppSharedPreferences.build(context, true);
}
public static synchronized TermuxAPIPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new TermuxAPIPreferencesDataStore(context);
}
return mInstance;
}
}

View File

@@ -0,0 +1,49 @@
package com.termux.app.fragments.settings;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Keep;
import androidx.preference.PreferenceDataStore;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import com.termux.R;
import com.termux.shared.settings.preferences.TermuxFloatAppSharedPreferences;
@Keep
public class TermuxFloatPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(TermuxFloatPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.termux_float_preferences, rootKey);
}
}
class TermuxFloatPreferencesDataStore extends PreferenceDataStore {
private final Context mContext;
private final TermuxFloatAppSharedPreferences mPreferences;
private static TermuxFloatPreferencesDataStore mInstance;
private TermuxFloatPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxFloatAppSharedPreferences.build(context, true);
}
public static synchronized TermuxFloatPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new TermuxFloatPreferencesDataStore(context);
}
return mInstance;
}
}

View File

@@ -0,0 +1,49 @@
package com.termux.app.fragments.settings;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Keep;
import androidx.preference.PreferenceDataStore;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import com.termux.R;
import com.termux.shared.settings.preferences.TermuxWidgetAppSharedPreferences;
@Keep
public class TermuxWidgetPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(TermuxWidgetPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.termux_widget_preferences, rootKey);
}
}
class TermuxWidgetPreferencesDataStore extends PreferenceDataStore {
private final Context mContext;
private final TermuxWidgetAppSharedPreferences mPreferences;
private static TermuxWidgetPreferencesDataStore mInstance;
private TermuxWidgetPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxWidgetAppSharedPreferences.build(context, true);
}
public static synchronized TermuxWidgetPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new TermuxWidgetPreferencesDataStore(context);
}
return mInstance;
}
}

View File

@@ -0,0 +1,77 @@
package com.termux.app.fragments.settings.termux;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Keep;
import androidx.preference.PreferenceDataStore;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import com.termux.R;
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
@Keep
public class TerminalViewPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(TerminalViewPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.termux_terminal_view_preferences, rootKey);
}
}
class TerminalViewPreferencesDataStore extends PreferenceDataStore {
private final Context mContext;
private final TermuxAppSharedPreferences mPreferences;
private static TerminalViewPreferencesDataStore mInstance;
private TerminalViewPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxAppSharedPreferences.build(context, true);
}
public static synchronized TerminalViewPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new TerminalViewPreferencesDataStore(context);
}
return mInstance;
}
@Override
public void putBoolean(String key, boolean value) {
if (mPreferences == null) return;
if (key == null) return;
switch (key) {
case "terminal_margin_adjustment":
mPreferences.setTerminalMarginAdjustment(value);
break;
default:
break;
}
}
@Override
public boolean getBoolean(String key, boolean defValue) {
if (mPreferences == null) return false;
switch (key) {
case "terminal_margin_adjustment":
return mPreferences.isTerminalMarginAdjustmentEnabled();
default:
return false;
}
}
}

View File

@@ -0,0 +1,101 @@
package com.termux.app.fragments.settings.termux_api;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.ListPreference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceDataStore;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import com.termux.R;
import com.termux.shared.settings.preferences.TermuxAPIAppSharedPreferences;
@Keep
public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.termux_api_debugging_preferences, rootKey);
configureLoggingPreferences(context);
}
private void configureLoggingPreferences(@NonNull Context context) {
PreferenceCategory loggingCategory = findPreference("logging");
if (loggingCategory == null) return;
ListPreference logLevelListPreference = findPreference("log_level");
if (logLevelListPreference != null) {
TermuxAPIAppSharedPreferences preferences = TermuxAPIAppSharedPreferences.build(context, true);
if (preferences == null) return;
com.termux.app.fragments.settings.termux.DebuggingPreferencesFragment.
setLogLevelListPreferenceData(logLevelListPreference, context, preferences.getLogLevel(true));
loggingCategory.addPreference(logLevelListPreference);
}
}
}
class DebuggingPreferencesDataStore extends PreferenceDataStore {
private final Context mContext;
private final TermuxAPIAppSharedPreferences mPreferences;
private static DebuggingPreferencesDataStore mInstance;
private DebuggingPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxAPIAppSharedPreferences.build(context, true);
}
public static synchronized DebuggingPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new DebuggingPreferencesDataStore(context);
}
return mInstance;
}
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
if (mPreferences == null) return null;
if (key == null) return null;
switch (key) {
case "log_level":
return String.valueOf(mPreferences.getLogLevel(true));
default:
return null;
}
}
@Override
public void putString(String key, @Nullable String value) {
if (mPreferences == null) return;
if (key == null) return;
switch (key) {
case "log_level":
if (value != null) {
mPreferences.setLogLevel(mContext, Integer.parseInt(value), true);
}
break;
default:
break;
}
}
}

View File

@@ -0,0 +1,126 @@
package com.termux.app.fragments.settings.termux_float;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.ListPreference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceDataStore;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import com.termux.R;
import com.termux.shared.settings.preferences.TermuxFloatAppSharedPreferences;
@Keep
public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.termux_float_debugging_preferences, rootKey);
configureLoggingPreferences(context);
}
private void configureLoggingPreferences(@NonNull Context context) {
PreferenceCategory loggingCategory = findPreference("logging");
if (loggingCategory == null) return;
ListPreference logLevelListPreference = findPreference("log_level");
if (logLevelListPreference != null) {
TermuxFloatAppSharedPreferences preferences = TermuxFloatAppSharedPreferences.build(context, true);
if (preferences == null) return;
com.termux.app.fragments.settings.termux.DebuggingPreferencesFragment.
setLogLevelListPreferenceData(logLevelListPreference, context, preferences.getLogLevel(true));
loggingCategory.addPreference(logLevelListPreference);
}
}
}
class DebuggingPreferencesDataStore extends PreferenceDataStore {
private final Context mContext;
private final TermuxFloatAppSharedPreferences mPreferences;
private static DebuggingPreferencesDataStore mInstance;
private DebuggingPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxFloatAppSharedPreferences.build(context, true);
}
public static synchronized DebuggingPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new DebuggingPreferencesDataStore(context);
}
return mInstance;
}
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
if (mPreferences == null) return null;
if (key == null) return null;
switch (key) {
case "log_level":
return String.valueOf(mPreferences.getLogLevel(true));
default:
return null;
}
}
@Override
public void putString(String key, @Nullable String value) {
if (mPreferences == null) return;
if (key == null) return;
switch (key) {
case "log_level":
if (value != null) {
mPreferences.setLogLevel(mContext, Integer.parseInt(value), true);
}
break;
default:
break;
}
}
@Override
public void putBoolean(String key, boolean value) {
if (mPreferences == null) return;
if (key == null) return;
switch (key) {
case "terminal_view_key_logging_enabled":
mPreferences.setTerminalViewKeyLoggingEnabled(value, true);
break;
default:
break;
}
}
@Override
public boolean getBoolean(String key, boolean defValue) {
if (mPreferences == null) return false;
switch (key) {
case "terminal_view_key_logging_enabled":
return mPreferences.isTerminalViewKeyLoggingEnabled(true);
default:
return false;
}
}
}

View File

@@ -0,0 +1,101 @@
package com.termux.app.fragments.settings.termux_widget;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.ListPreference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceDataStore;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import com.termux.R;
import com.termux.shared.settings.preferences.TermuxWidgetAppSharedPreferences;
@Keep
public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.termux_widget_debugging_preferences, rootKey);
configureLoggingPreferences(context);
}
private void configureLoggingPreferences(@NonNull Context context) {
PreferenceCategory loggingCategory = findPreference("logging");
if (loggingCategory == null) return;
ListPreference logLevelListPreference = findPreference("log_level");
if (logLevelListPreference != null) {
TermuxWidgetAppSharedPreferences preferences = TermuxWidgetAppSharedPreferences.build(context, true);
if (preferences == null) return;
com.termux.app.fragments.settings.termux.DebuggingPreferencesFragment.
setLogLevelListPreferenceData(logLevelListPreference, context, preferences.getLogLevel(true));
loggingCategory.addPreference(logLevelListPreference);
}
}
}
class DebuggingPreferencesDataStore extends PreferenceDataStore {
private final Context mContext;
private final TermuxWidgetAppSharedPreferences mPreferences;
private static DebuggingPreferencesDataStore mInstance;
private DebuggingPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxWidgetAppSharedPreferences.build(context, true);
}
public static synchronized DebuggingPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new DebuggingPreferencesDataStore(context);
}
return mInstance;
}
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
if (mPreferences == null) return null;
if (key == null) return null;
switch (key) {
case "log_level":
return String.valueOf(mPreferences.getLogLevel(true));
default:
return null;
}
}
@Override
public void putString(String key, @Nullable String value) {
if (mPreferences == null) return;
if (key == null) return;
switch (key) {
case "log_level":
if (value != null) {
mPreferences.setLogLevel(mContext, Integer.parseInt(value), true);
}
break;
default:
break;
}
}
}

View File

@@ -2,11 +2,16 @@ package com.termux.app.settings.properties;
import android.content.Context; import android.content.Context;
import androidx.annotation.NonNull;
import com.termux.app.terminal.io.KeyboardShortcut; import com.termux.app.terminal.io.KeyboardShortcut;
import com.termux.app.terminal.io.extrakeys.ExtraKeysInfo; import com.termux.shared.terminal.io.extrakeys.ExtraKeysConstants;
import com.termux.shared.terminal.io.extrakeys.ExtraKeysConstants.EXTRA_KEY_DISPLAY_MAPS;
import com.termux.shared.terminal.io.extrakeys.ExtraKeysInfo;
import com.termux.shared.logger.Logger; import com.termux.shared.logger.Logger;
import com.termux.shared.settings.properties.TermuxPropertyConstants; import com.termux.shared.settings.properties.TermuxPropertyConstants;
import com.termux.shared.settings.properties.TermuxSharedProperties; import com.termux.shared.settings.properties.TermuxSharedProperties;
import com.termux.shared.termux.TermuxConstants;
import org.json.JSONException; import org.json.JSONException;
@@ -14,8 +19,6 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import javax.annotation.Nonnull;
public class TermuxAppSharedProperties extends TermuxSharedProperties { public class TermuxAppSharedProperties extends TermuxSharedProperties {
private ExtraKeysInfo mExtraKeysInfo; private ExtraKeysInfo mExtraKeysInfo;
@@ -23,8 +26,9 @@ public class TermuxAppSharedProperties extends TermuxSharedProperties {
private static final String LOG_TAG = "TermuxAppSharedProperties"; private static final String LOG_TAG = "TermuxAppSharedProperties";
public TermuxAppSharedProperties(@Nonnull Context context) { public TermuxAppSharedProperties(@NonNull Context context) {
super(context); super(context, TermuxConstants.TERMUX_APP_NAME, TermuxPropertyConstants.getTermuxPropertiesFile(),
TermuxPropertyConstants.TERMUX_PROPERTIES_LIST, new SharedPropertiesParserClient());
} }
/** /**
@@ -50,13 +54,20 @@ public class TermuxAppSharedProperties extends TermuxSharedProperties {
// {@link #getExtraKeysStyleInternalPropertyValueFromValue(String)} // {@link #getExtraKeysStyleInternalPropertyValueFromValue(String)}
String extrakeys = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS, true); String extrakeys = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS, true);
String extraKeysStyle = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE, true); String extraKeysStyle = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE, true);
mExtraKeysInfo = new ExtraKeysInfo(extrakeys, extraKeysStyle);
ExtraKeysConstants.ExtraKeyDisplayMap extraKeyDisplayMap = ExtraKeysInfo.getCharDisplayMapForStyle(extraKeysStyle);
if (EXTRA_KEY_DISPLAY_MAPS.DEFAULT_CHAR_DISPLAY.equals(extraKeyDisplayMap) && !TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE.equals(extraKeysStyle)) {
Logger.logError(TermuxSharedProperties.LOG_TAG, "The style \"" + extraKeysStyle + "\" for the key \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE + "\" is invalid. Using default style instead.");
extraKeysStyle = TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE;
}
mExtraKeysInfo = new ExtraKeysInfo(extrakeys, extraKeysStyle, ExtraKeysConstants.CONTROL_CHARS_ALIASES);
} catch (JSONException e) { } catch (JSONException e) {
Logger.showToast(mContext, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: " + e.toString(), true); Logger.showToast(mContext, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: " + e.toString(), true);
Logger.logStackTraceWithMessage(LOG_TAG, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: ", e); Logger.logStackTraceWithMessage(LOG_TAG, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: ", e);
try { try {
mExtraKeysInfo = new ExtraKeysInfo(TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE); mExtraKeysInfo = new ExtraKeysInfo(TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE, ExtraKeysConstants.CONTROL_CHARS_ALIASES);
} catch (JSONException e2) { } catch (JSONException e2) {
Logger.showToast(mContext, "Can't create default extra keys",true); Logger.showToast(mContext, "Can't create default extra keys",true);
Logger.logStackTraceWithMessage(LOG_TAG, "Could create default extra keys: ", e); Logger.logStackTraceWithMessage(LOG_TAG, "Could create default extra keys: ", e);
@@ -101,7 +112,8 @@ public class TermuxAppSharedProperties extends TermuxSharedProperties {
* Load the {@link TermuxPropertyConstants#KEY_TERMINAL_TRANSCRIPT_ROWS} value from termux properties file on disk. * Load the {@link TermuxPropertyConstants#KEY_TERMINAL_TRANSCRIPT_ROWS} value from termux properties file on disk.
*/ */
public static int getTerminalTranscriptRows(Context context) { public static int getTerminalTranscriptRows(Context context) {
return (int) TermuxSharedProperties.getInternalPropertyValue(context, TermuxPropertyConstants.KEY_TERMINAL_TRANSCRIPT_ROWS); return (int) TermuxSharedProperties.getInternalPropertyValue(context, TermuxPropertyConstants.getTermuxPropertiesFile(),
TermuxPropertyConstants.KEY_TERMINAL_TRANSCRIPT_ROWS, new SharedPropertiesParserClient());
} }
} }

View File

@@ -20,7 +20,7 @@ import com.termux.shared.terminal.TermuxTerminalSessionClientBase;
import com.termux.shared.termux.TermuxConstants; import com.termux.shared.termux.TermuxConstants;
import com.termux.app.TermuxService; import com.termux.app.TermuxService;
import com.termux.shared.settings.properties.TermuxPropertyConstants; import com.termux.shared.settings.properties.TermuxPropertyConstants;
import com.termux.app.terminal.io.BellHandler; import com.termux.shared.terminal.io.BellHandler;
import com.termux.shared.logger.Logger; import com.termux.shared.logger.Logger;
import com.termux.terminal.TerminalColors; import com.termux.terminal.TerminalColors;
import com.termux.terminal.TerminalSession; import com.termux.terminal.TerminalSession;
@@ -138,37 +138,61 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
return; return;
} }
int index = service.getIndexOfSession(finishedSession);
// For plugin commands that expect the result back, we should immediately close the session
// and send the result back instead of waiting fo the user to press enter.
// The plugin can handle/show errors itself.
boolean isPluginExecutionCommandWithPendingResult = false;
TermuxSession termuxSession = service.getTermuxSession(index);
if (termuxSession != null) {
isPluginExecutionCommandWithPendingResult = termuxSession.getExecutionCommand().isPluginExecutionCommandWithPendingResult();
if (isPluginExecutionCommandWithPendingResult)
Logger.logVerbose(LOG_TAG, "The \"" + finishedSession.mSessionName + "\" session will be force finished automatically since result in pending.");
}
if (mActivity.isVisible() && finishedSession != mActivity.getCurrentSession()) { if (mActivity.isVisible() && finishedSession != mActivity.getCurrentSession()) {
// Show toast for non-current sessions that exit. // Show toast for non-current sessions that exit.
int indexOfSession = service.getIndexOfSession(finishedSession);
// Verify that session was not removed before we got told about it finishing: // Verify that session was not removed before we got told about it finishing:
if (indexOfSession >= 0) if (index >= 0)
mActivity.showToast(toToastTitle(finishedSession) + " - exited", true); mActivity.showToast(toToastTitle(finishedSession) + " - exited", true);
} }
if (mActivity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) { if (mActivity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
// On Android TV devices we need to use older behaviour because we may // On Android TV devices we need to use older behaviour because we may
// not be able to have multiple launcher icons. // not be able to have multiple launcher icons.
if (service.getTermuxSessionsSize() > 1) { if (service.getTermuxSessionsSize() > 1 || isPluginExecutionCommandWithPendingResult) {
removeFinishedSession(finishedSession); removeFinishedSession(finishedSession);
} }
} else { } else {
// Once we have a separate launcher icon for the failsafe session, it // Once we have a separate launcher icon for the failsafe session, it
// should be safe to auto-close session on exit code '0' or '130'. // should be safe to auto-close session on exit code '0' or '130'.
if (finishedSession.getExitStatus() == 0 || finishedSession.getExitStatus() == 130) { if (finishedSession.getExitStatus() == 0 || finishedSession.getExitStatus() == 130 || isPluginExecutionCommandWithPendingResult) {
removeFinishedSession(finishedSession); removeFinishedSession(finishedSession);
} }
} }
} }
@Override @Override
public void onClipboardText(TerminalSession session, String text) { public void onCopyTextToClipboard(TerminalSession session, String text) {
if (!mActivity.isVisible()) return; if (!mActivity.isVisible()) return;
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE); ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(text))); clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(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());
}
}
@Override @Override
public void onBell(TerminalSession session) { public void onBell(TerminalSession session) {
if (!mActivity.isVisible()) return; if (!mActivity.isVisible()) return;

View File

@@ -9,6 +9,7 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.media.AudioManager; import android.media.AudioManager;
import android.net.Uri; import android.net.Uri;
import android.os.Environment;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.Gravity; import android.view.Gravity;
import android.view.InputDevice; import android.view.InputDevice;
@@ -22,15 +23,18 @@ import android.widget.Toast;
import com.termux.R; import com.termux.R;
import com.termux.app.TermuxActivity; import com.termux.app.TermuxActivity;
import com.termux.shared.data.UrlUtils; import com.termux.shared.data.UrlUtils;
import com.termux.shared.file.FileUtils;
import com.termux.shared.interact.MessageDialogUtils;
import com.termux.shared.interact.ShareUtils;
import com.termux.shared.shell.ShellUtils; import com.termux.shared.shell.ShellUtils;
import com.termux.shared.terminal.TermuxTerminalViewClientBase; import com.termux.shared.terminal.TermuxTerminalViewClientBase;
import com.termux.shared.terminal.io.extrakeys.SpecialButton;
import com.termux.shared.termux.AndroidUtils; import com.termux.shared.termux.AndroidUtils;
import com.termux.shared.termux.TermuxConstants; import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.activities.ReportActivity; import com.termux.shared.activities.ReportActivity;
import com.termux.shared.models.ReportInfo; import com.termux.shared.models.ReportInfo;
import com.termux.app.models.UserAction; import com.termux.app.models.UserAction;
import com.termux.app.terminal.io.KeyboardShortcut; import com.termux.app.terminal.io.KeyboardShortcut;
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
import com.termux.shared.settings.properties.TermuxPropertyConstants; import com.termux.shared.settings.properties.TermuxPropertyConstants;
import com.termux.shared.data.DataUtils; import com.termux.shared.data.DataUtils;
import com.termux.shared.logger.Logger; import com.termux.shared.logger.Logger;
@@ -39,6 +43,7 @@ import com.termux.shared.termux.TermuxUtils;
import com.termux.shared.view.KeyboardUtils; import com.termux.shared.view.KeyboardUtils;
import com.termux.shared.view.ViewUtils; import com.termux.shared.view.ViewUtils;
import com.termux.terminal.KeyHandler; import com.termux.terminal.KeyHandler;
import com.termux.terminal.TerminalBuffer;
import com.termux.terminal.TerminalEmulator; import com.termux.terminal.TerminalEmulator;
import com.termux.terminal.TerminalSession; import com.termux.terminal.TerminalSession;
@@ -72,6 +77,10 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
this.mTermuxTerminalSessionClient = termuxTerminalSessionClient; this.mTermuxTerminalSessionClient = termuxTerminalSessionClient;
} }
public TermuxActivity getActivity() {
return mActivity;
}
/** /**
* Should be called when mActivity.onCreate() is called * Should be called when mActivity.onCreate() is called
*/ */
@@ -133,7 +142,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
} }
/** /**
* Should be called when {@link com.termux.view.TerminalView#mEmulator} * Should be called when {@link com.termux.view.TerminalView#mEmulator} is set
*/ */
@Override @Override
public void onEmulatorSet() { public void onEmulatorSet() {
@@ -165,10 +174,26 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
@Override @Override
public void onSingleTapUp(MotionEvent e) { public void onSingleTapUp(MotionEvent e) {
if (!KeyboardUtils.areDisableSoftKeyboardFlagsSet(mActivity)) TerminalEmulator term = mActivity.getCurrentSession().getEmulator();
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
else if (mActivity.getProperties().shouldOpenTerminalTranscriptURLOnClick()) {
Logger.logVerbose(LOG_TAG, "Not showing soft keyboard onSingleTapUp since its disabled"); int[] columnAndRow = mActivity.getTerminalView().getColumnAndRow(e, true);
String wordAtTap = term.getScreen().getWordAtLocation(columnAndRow[0], columnAndRow[1]);
LinkedHashSet<CharSequence> urlSet = UrlUtils.extractUrls(wordAtTap);
if (!urlSet.isEmpty()) {
String url = (String) urlSet.iterator().next();
ShareUtils.openURL(mActivity, url);
return;
}
}
if (!term.isMouseTrackingActive() && !e.isFromSource(InputDevice.SOURCE_MOUSE)) {
if (!KeyboardUtils.areDisableSoftKeyboardFlagsSet(mActivity))
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
else
Logger.logVerbose(LOG_TAG, "Not showing soft keyboard onSingleTapUp since its disabled");
}
} }
@Override @Override
@@ -186,6 +211,11 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
return mActivity.getProperties().isUsingCtrlSpaceWorkaround(); return mActivity.getProperties().isUsingCtrlSpaceWorkaround();
} }
@Override
public boolean isTerminalViewSelected() {
return mActivity.getTerminalToolbarViewPager() == null || mActivity.isTerminalViewSelected() || mActivity.getTerminalView().hasFocus();
}
@Override @Override
@@ -204,7 +234,8 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) { if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) {
mTermuxTerminalSessionClient.removeFinishedSession(currentSession); mTermuxTerminalSessionClient.removeFinishedSession(currentSession);
return true; return true;
} else if (e.isCtrlPressed() && e.isAltPressed()) { } else if (!mActivity.getProperties().areHardwareKeyboardShortcutsDisabled() &&
e.isCtrlPressed() && e.isAltPressed()) {
// Get the unmodified code point: // Get the unmodified code point:
int unicodeChar = e.getUnicodeChar(0); int unicodeChar = e.getUnicodeChar(0);
@@ -281,12 +312,32 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
@Override @Override
public boolean readControlKey() { public boolean readControlKey() {
return (mActivity.getExtraKeysView() != null && mActivity.getExtraKeysView().readSpecialButton(ExtraKeysView.SpecialButton.CTRL)) || mVirtualControlKeyDown; return readExtraKeysSpecialButton(SpecialButton.CTRL) || mVirtualControlKeyDown;
} }
@Override @Override
public boolean readAltKey() { public boolean readAltKey() {
return (mActivity.getExtraKeysView() != null && mActivity.getExtraKeysView().readSpecialButton(ExtraKeysView.SpecialButton.ALT)); return readExtraKeysSpecialButton(SpecialButton.ALT);
}
@Override
public boolean readShiftKey() {
return readExtraKeysSpecialButton(SpecialButton.SHIFT);
}
@Override
public boolean readFnKey() {
return readExtraKeysSpecialButton(SpecialButton.FN);
}
public boolean readExtraKeysSpecialButton(SpecialButton specialButton) {
if (mActivity.getExtraKeysView() == null) return false;
Boolean state = mActivity.getExtraKeysView().readSpecialButton(specialButton, true);
if (state == null) {
Logger.logError(LOG_TAG,"Failed to read an unregistered " + specialButton + " special button value from extra keys.");
return false;
}
return state;
} }
@Override @Override
@@ -484,7 +535,14 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
} }
public void setSoftKeyboardState(boolean isStartup, boolean isReloadTermuxProperties) { public void setSoftKeyboardState(boolean isStartup, boolean isReloadTermuxProperties) {
boolean noRequestFocus = false; boolean noShowKeyboard = false;
// Requesting terminal view focus is necessary regardless of if soft keyboard is to be
// disabled or hidden at startup, otherwise if hardware keyboard is attached and user
// starts typing on hardware keyboard without tapping on the terminal first, then a colour
// tint will be added to the terminal as highlight for the focussed view. Test with a light
// theme. For android 8.+, the "defaultFocusHighlightEnabled" attribute is also set to false
// in TerminalView layout to fix the issue.
// If soft keyboard is disabled by user for Termux (check function docs for Termux behaviour info) // If soft keyboard is disabled by user for Termux (check function docs for Termux behaviour info)
if (KeyboardUtils.shouldSoftKeyboardBeDisabled(mActivity, if (KeyboardUtils.shouldSoftKeyboardBeDisabled(mActivity,
@@ -492,7 +550,8 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
mActivity.getPreferences().isSoftKeyboardEnabledOnlyIfNoHardware())) { mActivity.getPreferences().isSoftKeyboardEnabledOnlyIfNoHardware())) {
Logger.logVerbose(LOG_TAG, "Maintaining disabled soft keyboard"); Logger.logVerbose(LOG_TAG, "Maintaining disabled soft keyboard");
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView()); KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
noRequestFocus = true; mActivity.getTerminalView().requestFocus();
noShowKeyboard = true;
// Delay is only required if onCreate() is called like when Termux app is exited with // Delay is only required if onCreate() is called like when Termux app is exited with
// double back press, not when Termux app is switched back from another app and keyboard // double back press, not when Termux app is switched back from another app and keyboard
// toggle is pressed to enable keyboard // toggle is pressed to enable keyboard
@@ -508,10 +567,12 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
// If soft keyboard is to be hidden on startup // If soft keyboard is to be hidden on startup
if (isStartup && mActivity.getProperties().shouldSoftKeyboardBeHiddenOnStartup()) { if (isStartup && mActivity.getProperties().shouldSoftKeyboardBeHiddenOnStartup()) {
Logger.logVerbose(LOG_TAG, "Hiding soft keyboard on startup"); Logger.logVerbose(LOG_TAG, "Hiding soft keyboard on startup");
KeyboardUtils.hideSoftKeyboard(mActivity, mActivity.getTerminalView());
// Required to keep keyboard hidden when Termux app is switched back from another app // Required to keep keyboard hidden when Termux app is switched back from another app
KeyboardUtils.setSoftKeyboardAlwaysHiddenFlags(mActivity); KeyboardUtils.setSoftKeyboardAlwaysHiddenFlags(mActivity);
noRequestFocus = true;
KeyboardUtils.hideSoftKeyboard(mActivity, mActivity.getTerminalView());
mActivity.getTerminalView().requestFocus();
noShowKeyboard = true;
// Required to keep keyboard hidden on app startup // Required to keep keyboard hidden on app startup
mShowSoftKeyboardIgnoreOnce = true; mShowSoftKeyboardIgnoreOnce = true;
} }
@@ -541,7 +602,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
// Do not force show soft keyboard if termux-reload-settings command was run with hardware keyboard // Do not force show soft keyboard if termux-reload-settings command was run with hardware keyboard
// or soft keyboard is to be hidden or is disabled // or soft keyboard is to be hidden or is disabled
if (!isReloadTermuxProperties && !noRequestFocus) { if (!isReloadTermuxProperties && !noShowKeyboard) {
// Request focus for TerminalView // Request focus for TerminalView
// Also show the keyboard, since onFocusChange will not be called if TerminalView already // Also show the keyboard, since onFocusChange will not be called if TerminalView already
// had focus on startup to show the keyboard, like when opening url with context menu // had focus on startup to show the keyboard, like when opening url with context menu
@@ -628,13 +689,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
lv.setOnItemLongClickListener((parent, view, position, id) -> { lv.setOnItemLongClickListener((parent, view, position, id) -> {
dialog.dismiss(); dialog.dismiss();
String url = (String) urls[position]; String url = (String) urls[position];
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); ShareUtils.openURL(mActivity, url);
try {
mActivity.startActivity(i, null);
} catch (ActivityNotFoundException e) {
// If no applications match, Android displays a system message.
mActivity.startActivity(Intent.createChooser(i, null));
}
return true; return true;
}); });
}); });
@@ -649,20 +704,26 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
final String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true); final String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
if (transcriptText == null) return; if (transcriptText == null) return;
MessageDialogUtils.showMessage(mActivity, TermuxConstants.TERMUX_APP_NAME + " Report Issue",
mActivity.getString(R.string.msg_add_termux_debug_info),
mActivity.getString(R.string.action_yes), (dialog, which) -> reportIssueFromTranscript(transcriptText, true),
mActivity.getString(R.string.action_no), (dialog, which) -> reportIssueFromTranscript(transcriptText, false),
null);
}
private void reportIssueFromTranscript(String transcriptText, boolean addTermuxDebugInfo) {
Logger.showToast(mActivity, mActivity.getString(R.string.msg_generating_report), true); Logger.showToast(mActivity, mActivity.getString(R.string.msg_generating_report), true);
new Thread() { new Thread() {
@Override @Override
public void run() { public void run() {
String transcriptTextTruncated = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
StringBuilder reportString = new StringBuilder(); StringBuilder reportString = new StringBuilder();
String title = TermuxConstants.TERMUX_APP_NAME + " Report Issue"; String title = TermuxConstants.TERMUX_APP_NAME + " Report Issue";
reportString.append("## Transcript\n"); reportString.append("## Transcript\n");
reportString.append("\n").append(MarkdownUtils.getMarkdownCodeForString(transcriptTextTruncated, true)); reportString.append("\n").append(MarkdownUtils.getMarkdownCodeForString(transcriptText, true));
reportString.append("\n##\n");
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, true)); reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, true));
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(mActivity)); reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(mActivity));
@@ -671,7 +732,21 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
if (termuxAptInfo != null) if (termuxAptInfo != null)
reportString.append("\n\n").append(termuxAptInfo); reportString.append("\n\n").append(termuxAptInfo);
ReportActivity.startReportActivity(mActivity, new ReportInfo(UserAction.REPORT_ISSUE_FROM_TRANSCRIPT.getName(), TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY_NAME, title, null, reportString.toString(), "\n\n" + TermuxUtils.getReportIssueMarkdownString(mActivity), false)); if (addTermuxDebugInfo) {
String termuxDebugInfo = TermuxUtils.getTermuxDebugMarkdownString(mActivity);
if (termuxDebugInfo != null)
reportString.append("\n\n").append(termuxDebugInfo);
}
String userActionName = UserAction.REPORT_ISSUE_FROM_TRANSCRIPT.getName();
ReportActivity.startReportActivity(mActivity,
new ReportInfo(userActionName,
TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY_NAME, title, null,
reportString.toString(), "\n\n" + TermuxUtils.getReportIssueMarkdownString(mActivity),
false,
userActionName,
Environment.getExternalStorageDirectory() + "/" +
FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true)));
} }
}.start(); }.start();
} }

View File

@@ -11,7 +11,7 @@ import androidx.viewpager.widget.ViewPager;
import com.termux.R; import com.termux.R;
import com.termux.app.TermuxActivity; import com.termux.app.TermuxActivity;
import com.termux.app.terminal.io.extrakeys.ExtraKeysView; import com.termux.shared.terminal.io.extrakeys.ExtraKeysView;
import com.termux.terminal.TerminalSession; import com.termux.terminal.TerminalSession;
public class TerminalToolbarViewPager { public class TerminalToolbarViewPager {
@@ -44,7 +44,9 @@ public class TerminalToolbarViewPager {
if (position == 0) { if (position == 0) {
layout = inflater.inflate(R.layout.view_terminal_toolbar_extra_keys, collection, false); layout = inflater.inflate(R.layout.view_terminal_toolbar_extra_keys, collection, false);
ExtraKeysView extraKeysView = (ExtraKeysView) layout; ExtraKeysView extraKeysView = (ExtraKeysView) layout;
extraKeysView.setTermuxTerminalViewClient(mActivity.getTermuxTerminalViewClient()); extraKeysView.setExtraKeysViewClient(new TermuxTerminalExtraKeys(mActivity.getTerminalView(),
mActivity.getTermuxTerminalViewClient(), mActivity.getTermuxTerminalSessionClient()));
extraKeysView.setButtonTextAllCaps(mActivity.getProperties().shouldExtraKeysTextBeAllCaps());
mActivity.setExtraKeysView(extraKeysView); mActivity.setExtraKeysView(extraKeysView);
extraKeysView.reload(mActivity.getProperties().getExtraKeysInfo()); extraKeysView.reload(mActivity.getProperties().getExtraKeysInfo());

View File

@@ -0,0 +1,49 @@
package com.termux.app.terminal.io;
import android.annotation.SuppressLint;
import android.view.Gravity;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.drawerlayout.widget.DrawerLayout;
import com.termux.app.terminal.TermuxTerminalSessionClient;
import com.termux.app.terminal.TermuxTerminalViewClient;
import com.termux.shared.terminal.io.TerminalExtraKeys;
import com.termux.view.TerminalView;
public class TermuxTerminalExtraKeys extends TerminalExtraKeys {
TermuxTerminalViewClient mTermuxTerminalViewClient;
TermuxTerminalSessionClient mTermuxTerminalSessionClient;
public TermuxTerminalExtraKeys(@NonNull TerminalView terminalView,
TermuxTerminalViewClient termuxTerminalViewClient,
TermuxTerminalSessionClient termuxTerminalSessionClient) {
super(terminalView);
mTermuxTerminalViewClient = termuxTerminalViewClient;
mTermuxTerminalSessionClient = termuxTerminalSessionClient;
}
@SuppressLint("RtlHardcoded")
@Override
public void onTerminalExtraKeyButtonClick(View view, String key, boolean ctrlDown, boolean altDown, boolean shiftDown, boolean fnDown) {
if ("KEYBOARD".equals(key)) {
if(mTermuxTerminalViewClient != null)
mTermuxTerminalViewClient.onToggleSoftKeyboardRequest();
} else if ("DRAWER".equals(key)) {
DrawerLayout drawerLayout = mTermuxTerminalViewClient.getActivity().getDrawer();
if (drawerLayout.isDrawerOpen(Gravity.LEFT))
drawerLayout.closeDrawer(Gravity.LEFT);
else
drawerLayout.openDrawer(Gravity.LEFT);
} else if ("PASTE".equals(key)) {
if(mTermuxTerminalSessionClient != null)
mTermuxTerminalSessionClient.onPasteTextFromClipboard(null);
} else {
super.onTerminalExtraKeyButtonClick(view, key, ctrlDown, altDown, shiftDown, fnDown);
}
}
}

View File

@@ -1,92 +0,0 @@
package com.termux.app.terminal.io.extrakeys;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Arrays;
import java.util.stream.Collectors;
public class ExtraKeyButton {
/**
* The key that will be sent to the terminal, either a control character
* defined in ExtraKeysView.keyCodesForString (LEFT, RIGHT, PGUP...) or
* some text.
*/
private final String key;
/**
* If the key is a macro, i.e. a sequence of keys separated by space.
*/
private final boolean macro;
/**
* The text that will be shown on the button.
*/
private final String display;
/**
* The information of the popup (triggered by swipe up).
*/
@Nullable
private ExtraKeyButton popup;
public ExtraKeyButton(ExtraKeysInfo.CharDisplayMap charDisplayMap, JSONObject config) throws JSONException {
this(charDisplayMap, config, null);
}
public ExtraKeyButton(ExtraKeysInfo.CharDisplayMap charDisplayMap, JSONObject config, @Nullable ExtraKeyButton popup) throws JSONException {
String keyFromConfig = config.optString("key", null);
String macroFromConfig = config.optString("macro", null);
String[] keys;
if (keyFromConfig != null && macroFromConfig != null) {
throw new JSONException("Both key and macro can't be set for the same key");
} else if (keyFromConfig != null) {
keys = new String[]{keyFromConfig};
this.macro = false;
} else if (macroFromConfig != null) {
keys = macroFromConfig.split(" ");
this.macro = true;
} else {
throw new JSONException("All keys have to specify either key or macro");
}
for (int i = 0; i < keys.length; i++) {
keys[i] = ExtraKeysInfo.replaceAlias(keys[i]);
}
this.key = TextUtils.join(" ", keys);
String displayFromConfig = config.optString("display", null);
if (displayFromConfig != null) {
this.display = displayFromConfig;
} else {
this.display = Arrays.stream(keys)
.map(key -> charDisplayMap.get(key, key))
.collect(Collectors.joining(" "));
}
this.popup = popup;
}
public String getKey() {
return key;
}
public boolean isMacro() {
return macro;
}
public String getDisplay() {
return display;
}
@Nullable
public ExtraKeyButton getPopup() {
return popup;
}
}

View File

@@ -1,259 +0,0 @@
package com.termux.app.terminal.io.extrakeys;
import com.termux.shared.logger.Logger;
import com.termux.shared.settings.properties.TermuxPropertyConstants;
import com.termux.shared.settings.properties.TermuxSharedProperties;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashMap;
public class ExtraKeysInfo {
/**
* Matrix of buttons displayed
*/
private final ExtraKeyButton[][] buttons;
/**
* This corresponds to one of the CharMapDisplay below
*/
private String style;
public ExtraKeysInfo(String propertiesInfo, String style) throws JSONException {
this.style = style;
// Convert String propertiesInfo to Array of Arrays
JSONArray arr = new JSONArray(propertiesInfo);
Object[][] matrix = new Object[arr.length()][];
for (int i = 0; i < arr.length(); i++) {
JSONArray line = arr.getJSONArray(i);
matrix[i] = new Object[line.length()];
for (int j = 0; j < line.length(); j++) {
matrix[i][j] = line.get(j);
}
}
// convert matrix to buttons
this.buttons = new ExtraKeyButton[matrix.length][];
for (int i = 0; i < matrix.length; i++) {
this.buttons[i] = new ExtraKeyButton[matrix[i].length];
for (int j = 0; j < matrix[i].length; j++) {
Object key = matrix[i][j];
JSONObject jobject = normalizeKeyConfig(key);
ExtraKeyButton button;
if (! jobject.has("popup")) {
// no popup
button = new ExtraKeyButton(getSelectedCharMap(), jobject);
} else {
// a popup
JSONObject popupJobject = normalizeKeyConfig(jobject.get("popup"));
ExtraKeyButton popup = new ExtraKeyButton(getSelectedCharMap(), popupJobject);
button = new ExtraKeyButton(getSelectedCharMap(), jobject, popup);
}
this.buttons[i][j] = button;
}
}
}
/**
* "hello" -> {"key": "hello"}
*/
private static JSONObject normalizeKeyConfig(Object key) throws JSONException {
JSONObject jobject;
if (key instanceof String) {
jobject = new JSONObject();
jobject.put("key", key);
} else if (key instanceof JSONObject) {
jobject = (JSONObject) key;
} else {
throw new JSONException("An key in the extra-key matrix must be a string or an object");
}
return jobject;
}
public ExtraKeyButton[][] getMatrix() {
return buttons;
}
/**
* HashMap that implements Python dict.get(key, default) function.
* Default java.util .get(key) is then the same as .get(key, null);
*/
static class CleverMap<K,V> extends HashMap<K,V> {
V get(K key, V defaultValue) {
if (containsKey(key))
return get(key);
else
return defaultValue;
}
}
static class CharDisplayMap extends CleverMap<String, String> {}
/**
* Keys are displayed in a natural looking way, like "→" for "RIGHT"
*/
static final CharDisplayMap classicArrowsDisplay = new CharDisplayMap() {{
// classic arrow keys (for ◀ ▶ ▲ ▼ @see arrowVariationDisplay)
put("LEFT", ""); // U+2190 ← LEFTWARDS ARROW
put("RIGHT", ""); // U+2192 → RIGHTWARDS ARROW
put("UP", ""); // U+2191 ↑ UPWARDS ARROW
put("DOWN", ""); // U+2193 ↓ DOWNWARDS ARROW
}};
static final CharDisplayMap wellKnownCharactersDisplay = new CharDisplayMap() {{
// well known characters // https://en.wikipedia.org/wiki/{Enter_key, Tab_key, Delete_key}
put("ENTER", ""); // U+21B2 ↲ DOWNWARDS ARROW WITH TIP LEFTWARDS
put("TAB", ""); // U+21B9 ↹ LEFTWARDS ARROW TO BAR OVER RIGHTWARDS ARROW TO BAR
put("BKSP", ""); // U+232B ⌫ ERASE TO THE LEFT sometimes seen and easy to understand
put("DEL", ""); // U+2326 ⌦ ERASE TO THE RIGHT not well known but easy to understand
put("DRAWER", ""); // U+2630 ☰ TRIGRAM FOR HEAVEN not well known but easy to understand
put("KEYBOARD", ""); // U+2328 ⌨ KEYBOARD not well known but easy to understand
}};
static final CharDisplayMap lessKnownCharactersDisplay = new CharDisplayMap() {{
// https://en.wikipedia.org/wiki/{Home_key, End_key, Page_Up_and_Page_Down_keys}
// home key can mean "goto the beginning of line" or "goto first page" depending on context, hence the diagonal
put("HOME", ""); // from IEC 9995 // U+21F1 ⇱ NORTH WEST ARROW TO CORNER
put("END", ""); // from IEC 9995 // ⇲ // U+21F2 ⇲ SOUTH EAST ARROW TO CORNER
put("PGUP", ""); // no ISO character exists, U+21D1 ⇑ UPWARDS DOUBLE ARROW will do the trick
put("PGDN", ""); // no ISO character exists, U+21D3 ⇓ DOWNWARDS DOUBLE ARROW will do the trick
}};
static final CharDisplayMap arrowTriangleVariationDisplay = new CharDisplayMap() {{
// alternative to classic arrow keys
put("LEFT", ""); // U+25C0 ◀ BLACK LEFT-POINTING TRIANGLE
put("RIGHT", ""); // U+25B6 ▶ BLACK RIGHT-POINTING TRIANGLE
put("UP", ""); // U+25B2 ▲ BLACK UP-POINTING TRIANGLE
put("DOWN", ""); // U+25BC ▼ BLACK DOWN-POINTING TRIANGLE
}};
static final CharDisplayMap notKnownIsoCharacters = new CharDisplayMap() {{
// Control chars that are more clear as text // https://en.wikipedia.org/wiki/{Function_key, Alt_key, Control_key, Esc_key}
// put("FN", "FN"); // no ISO character exists
put("CTRL", ""); // ISO character "U+2388 ⎈ HELM SYMBOL" is unknown to people and never printed on computers, however "U+25C7 ◇ WHITE DIAMOND" is a nice presentation, and "^" for terminal app and mac is often used
put("ALT", ""); // ISO character "U+2387 ⎇ ALTERNATIVE KEY SYMBOL'" is unknown to people and only printed as the Option key "⌥" on Mac computer
put("ESC", ""); // ISO character "U+238B ⎋ BROKEN CIRCLE WITH NORTHWEST ARROW" is unknown to people and not often printed on computers
}};
static final CharDisplayMap nicerLookingDisplay = new CharDisplayMap() {{
// nicer looking for most cases
put("-", ""); // U+2015 ― HORIZONTAL BAR
}};
/*
* Multiple maps are available to quickly change
* the style of the keys.
*/
/**
* Some classic symbols everybody knows
*/
private static final CharDisplayMap defaultCharDisplay = new CharDisplayMap() {{
putAll(classicArrowsDisplay);
putAll(wellKnownCharactersDisplay);
putAll(nicerLookingDisplay);
// all other characters are displayed as themselves
}};
/**
* Classic symbols and less known symbols
*/
private static final CharDisplayMap lotsOfArrowsCharDisplay = new CharDisplayMap() {{
putAll(classicArrowsDisplay);
putAll(wellKnownCharactersDisplay);
putAll(lessKnownCharactersDisplay); // NEW
putAll(nicerLookingDisplay);
}};
/**
* Only arrows
*/
private static final CharDisplayMap arrowsOnlyCharDisplay = new CharDisplayMap() {{
putAll(classicArrowsDisplay);
// putAll(wellKnownCharactersDisplay); // REMOVED
// putAll(lessKnownCharactersDisplay); // REMOVED
putAll(nicerLookingDisplay);
}};
/**
* Full Iso
*/
private static final CharDisplayMap fullIsoCharDisplay = new CharDisplayMap() {{
putAll(classicArrowsDisplay);
putAll(wellKnownCharactersDisplay);
putAll(lessKnownCharactersDisplay); // NEW
putAll(nicerLookingDisplay);
putAll(notKnownIsoCharacters); // NEW
}};
/**
* Some people might call our keys differently
*/
static private final CharDisplayMap controlCharsAliases = new CharDisplayMap() {{
put("ESCAPE", "ESC");
put("CONTROL", "CTRL");
put("RETURN", "ENTER"); // Technically different keys, but most applications won't see the difference
put("FUNCTION", "FN");
// no alias for ALT
// Directions are sometimes written as first and last letter for brevety
put("LT", "LEFT");
put("RT", "RIGHT");
put("DN", "DOWN");
// put("UP", "UP"); well, "UP" is already two letters
put("PAGEUP", "PGUP");
put("PAGE_UP", "PGUP");
put("PAGE UP", "PGUP");
put("PAGE-UP", "PGUP");
// no alias for HOME
// no alias for END
put("PAGEDOWN", "PGDN");
put("PAGE_DOWN", "PGDN");
put("PAGE-DOWN", "PGDN");
put("DELETE", "DEL");
put("BACKSPACE", "BKSP");
// easier for writing in termux.properties
put("BACKSLASH", "\\");
put("QUOTE", "\"");
put("APOSTROPHE", "'");
}};
CharDisplayMap getSelectedCharMap() {
switch (style) {
case "arrows-only":
return arrowsOnlyCharDisplay;
case "arrows-all":
return lotsOfArrowsCharDisplay;
case "all":
return fullIsoCharDisplay;
case "none":
return new CharDisplayMap();
default:
if (!TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE.equals(style))
Logger.logError(TermuxSharedProperties.LOG_TAG, "The style \"" + style + "\" for the key \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE + "\" is invalid. Using default style instead.");
return defaultCharDisplay;
}
}
/**
* Applies the 'controlCharsAliases' mapping to all the strings in *buttons*
* Modifies the array, doesn't return a new one.
*/
public static String replaceAlias(String key) {
return controlCharsAliases.get(key, key);
}
}

View File

@@ -1,388 +0,0 @@
package com.termux.app.terminal.io.extrakeys;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.provider.Settings;
import android.util.AttributeSet;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ScheduledExecutorService;
import java.util.Map;
import java.util.HashMap;
import java.util.Arrays;
import java.util.stream.Collectors;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.GridLayout;
import android.widget.PopupWindow;
import com.termux.R;
import com.termux.app.terminal.TermuxTerminalViewClient;
import com.termux.view.TerminalView;
import androidx.drawerlayout.widget.DrawerLayout;
/**
* A view showing extra keys (such as Escape, Ctrl, Alt) not normally available on an Android soft
* keyboard.
*/
public final class ExtraKeysView extends GridLayout {
private static final int TEXT_COLOR = 0xFFFFFFFF;
private static final int BUTTON_COLOR = 0x00000000;
private static final int INTERESTING_COLOR = 0xFF80DEEA;
private static final int BUTTON_PRESSED_COLOR = 0xFF7F7F7F;
TermuxTerminalViewClient mTermuxTerminalViewClient;
public ExtraKeysView(Context context, AttributeSet attrs) {
super(context, attrs);
}
static final Map<String, Integer> keyCodesForString = new HashMap<String, Integer>() {{
put("SPACE", KeyEvent.KEYCODE_SPACE);
put("ESC", KeyEvent.KEYCODE_ESCAPE);
put("TAB", KeyEvent.KEYCODE_TAB);
put("HOME", KeyEvent.KEYCODE_MOVE_HOME);
put("END", KeyEvent.KEYCODE_MOVE_END);
put("PGUP", KeyEvent.KEYCODE_PAGE_UP);
put("PGDN", KeyEvent.KEYCODE_PAGE_DOWN);
put("INS", KeyEvent.KEYCODE_INSERT);
put("DEL", KeyEvent.KEYCODE_FORWARD_DEL);
put("BKSP", KeyEvent.KEYCODE_DEL);
put("UP", KeyEvent.KEYCODE_DPAD_UP);
put("LEFT", KeyEvent.KEYCODE_DPAD_LEFT);
put("RIGHT", KeyEvent.KEYCODE_DPAD_RIGHT);
put("DOWN", KeyEvent.KEYCODE_DPAD_DOWN);
put("ENTER", KeyEvent.KEYCODE_ENTER);
put("F1", KeyEvent.KEYCODE_F1);
put("F2", KeyEvent.KEYCODE_F2);
put("F3", KeyEvent.KEYCODE_F3);
put("F4", KeyEvent.KEYCODE_F4);
put("F5", KeyEvent.KEYCODE_F5);
put("F6", KeyEvent.KEYCODE_F6);
put("F7", KeyEvent.KEYCODE_F7);
put("F8", KeyEvent.KEYCODE_F8);
put("F9", KeyEvent.KEYCODE_F9);
put("F10", KeyEvent.KEYCODE_F10);
put("F11", KeyEvent.KEYCODE_F11);
put("F12", KeyEvent.KEYCODE_F12);
}};
@SuppressLint("RtlHardcoded")
private void sendKey(View view, String keyName, boolean forceCtrlDown, boolean forceLeftAltDown) {
TerminalView terminalView = view.findViewById(R.id.terminal_view);
if ("KEYBOARD".equals(keyName)) {
if(mTermuxTerminalViewClient != null)
mTermuxTerminalViewClient.onToggleSoftKeyboardRequest();
} else if ("DRAWER".equals(keyName)) {
DrawerLayout drawer = view.findViewById(R.id.drawer_layout);
drawer.openDrawer(Gravity.LEFT);
} else if (keyCodesForString.containsKey(keyName)) {
Integer keyCode = keyCodesForString.get(keyName);
if (keyCode == null) return;
int metaState = 0;
if (forceCtrlDown) {
metaState |= KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON;
}
if (forceLeftAltDown) {
metaState |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON;
}
KeyEvent keyEvent = new KeyEvent(0, 0, KeyEvent.ACTION_UP, keyCode, 0, metaState);
terminalView.onKeyDown(keyCode, keyEvent);
} else {
// not a control char
keyName.codePoints().forEach(codePoint -> {
terminalView.inputCodePoint(codePoint, forceCtrlDown, forceLeftAltDown);
});
}
}
private void sendKey(View view, ExtraKeyButton buttonInfo) {
if (buttonInfo.isMacro()) {
String[] keys = buttonInfo.getKey().split(" ");
boolean ctrlDown = false;
boolean altDown = false;
for (String key : keys) {
if ("CTRL".equals(key)) {
ctrlDown = true;
} else if ("ALT".equals(key)) {
altDown = true;
} else {
sendKey(view, key, ctrlDown, altDown);
ctrlDown = false;
altDown = false;
}
}
} else {
sendKey(view, buttonInfo.getKey(), false, false);
}
}
public enum SpecialButton {
CTRL, ALT, FN
}
private static class SpecialButtonState {
boolean isOn = false;
boolean isActive = false;
List<Button> buttons = new ArrayList<>();
void setIsActive(boolean value) {
isActive = value;
buttons.forEach(button -> button.setTextColor(value ? INTERESTING_COLOR : TEXT_COLOR));
}
}
private final Map<SpecialButton, SpecialButtonState> specialButtons = new HashMap<SpecialButton, SpecialButtonState>() {{
put(SpecialButton.CTRL, new SpecialButtonState());
put(SpecialButton.ALT, new SpecialButtonState());
put(SpecialButton.FN, new SpecialButtonState());
}};
private final Set<String> specialButtonsKeys = specialButtons.keySet().stream().map(Enum::name).collect(Collectors.toSet());
private boolean isSpecialButton(ExtraKeyButton button) {
return specialButtonsKeys.contains(button.getKey());
}
private ScheduledExecutorService scheduledExecutor;
private PopupWindow popupWindow;
private int longPressCount;
public boolean readSpecialButton(SpecialButton name) {
SpecialButtonState state = specialButtons.get(name);
if (state == null)
throw new RuntimeException("Must be a valid special button (see source)");
if (!state.isOn || !state.isActive)
return false;
state.setIsActive(false);
return true;
}
private Button createSpecialButton(String buttonKey, boolean needUpdate) {
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonKey));
if (state == null) return null;
state.isOn = true;
Button button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
button.setTextColor(state.isActive ? INTERESTING_COLOR : TEXT_COLOR);
if (needUpdate) {
state.buttons.add(button);
}
return button;
}
void popup(View view, ExtraKeyButton extraButton) {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
Button button;
if (isSpecialButton(extraButton)) {
button = createSpecialButton(extraButton.getKey(), false);
if (button == null) return;
} else {
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
button.setTextColor(TEXT_COLOR);
}
button.setText(extraButton.getDisplay());
button.setPadding(0, 0, 0, 0);
button.setMinHeight(0);
button.setMinWidth(0);
button.setMinimumWidth(0);
button.setMinimumHeight(0);
button.setWidth(width);
button.setHeight(height);
button.setBackgroundColor(BUTTON_PRESSED_COLOR);
popupWindow = new PopupWindow(this);
popupWindow.setWidth(LayoutParams.WRAP_CONTENT);
popupWindow.setHeight(LayoutParams.WRAP_CONTENT);
popupWindow.setContentView(button);
popupWindow.setOutsideTouchable(true);
popupWindow.setFocusable(false);
popupWindow.showAsDropDown(view, 0, -2 * height);
}
/**
* General util function to compute the longest column length in a matrix.
*/
static int maximumLength(Object[][] matrix) {
int m = 0;
for (Object[] row : matrix)
m = Math.max(m, row.length);
return m;
}
/**
* Reload the view given parameters in termux.properties
*
* @param infos matrix as defined in termux.properties extrakeys
* Can Contain The Strings CTRL ALT TAB FN ENTER LEFT RIGHT UP DOWN or normal strings
* Some aliases are possible like RETURN for ENTER, LT for LEFT and more (@see controlCharsAliases for the whole list).
* Any string of length > 1 in total Uppercase will print a warning
*
* Examples:
* "ENTER" will trigger the ENTER keycode
* "LEFT" will trigger the LEFT keycode and be displayed as "←"
* "→" will input a "→" character
* "" will input a "" character
* "-_-" will input the string "-_-"
*/
@SuppressLint("ClickableViewAccessibility")
public void reload(ExtraKeysInfo infos) {
if (infos == null)
return;
for(SpecialButtonState state : specialButtons.values())
state.buttons = new ArrayList<>();
removeAllViews();
ExtraKeyButton[][] buttons = infos.getMatrix();
setRowCount(buttons.length);
setColumnCount(maximumLength(buttons));
for (int row = 0; row < buttons.length; row++) {
for (int col = 0; col < buttons[row].length; col++) {
final ExtraKeyButton buttonInfo = buttons[row][col];
Button button;
if (isSpecialButton(buttonInfo)) {
button = createSpecialButton(buttonInfo.getKey(), true);
if (button == null) return;
} else {
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
}
button.setText(buttonInfo.getDisplay());
button.setTextColor(TEXT_COLOR);
button.setPadding(0, 0, 0, 0);
final Button finalButton = button;
button.setOnClickListener(v -> {
if (Settings.System.getInt(getContext().getContentResolver(),
Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) != 0) {
if (Build.VERSION.SDK_INT >= 28) {
finalButton.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
} else {
// Perform haptic feedback only if no total silence mode enabled.
if (Settings.Global.getInt(getContext().getContentResolver(), "zen_mode", 0) != 2) {
finalButton.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
}
}
}
View root = getRootView();
if (isSpecialButton(buttonInfo)) {
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonInfo.getKey()));
if (state == null) return;
state.setIsActive(!state.isActive);
} else {
sendKey(root, buttonInfo);
}
});
button.setOnTouchListener((v, event) -> {
final View root = getRootView();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
longPressCount = 0;
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
if (Arrays.asList("UP", "DOWN", "LEFT", "RIGHT", "BKSP", "DEL").contains(buttonInfo.getKey())) {
// autorepeat
scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
scheduledExecutor.scheduleWithFixedDelay(() -> {
longPressCount++;
sendKey(root, buttonInfo);
}, 400, 80, TimeUnit.MILLISECONDS);
}
return true;
case MotionEvent.ACTION_MOVE:
if (buttonInfo.getPopup() != null) {
if (popupWindow == null && event.getY() < 0) {
if (scheduledExecutor != null) {
scheduledExecutor.shutdownNow();
scheduledExecutor = null;
}
v.setBackgroundColor(BUTTON_COLOR);
popup(v, buttonInfo.getPopup());
}
if (popupWindow != null && event.getY() > 0) {
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
popupWindow.dismiss();
popupWindow = null;
}
}
return true;
case MotionEvent.ACTION_CANCEL:
v.setBackgroundColor(BUTTON_COLOR);
if (scheduledExecutor != null) {
scheduledExecutor.shutdownNow();
scheduledExecutor = null;
}
return true;
case MotionEvent.ACTION_UP:
v.setBackgroundColor(BUTTON_COLOR);
if (scheduledExecutor != null) {
scheduledExecutor.shutdownNow();
scheduledExecutor = null;
}
if (longPressCount == 0 || popupWindow != null) {
if (popupWindow != null) {
popupWindow.setContentView(null);
popupWindow.dismiss();
popupWindow = null;
if (buttonInfo.getPopup() != null) {
if (isSpecialButton(buttonInfo.getPopup())) {
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonInfo.getPopup().getKey()));
if (state == null) return true;
state.setIsActive(!state.isActive);
} else {
sendKey(root, buttonInfo.getPopup());
}
}
} else {
v.performClick();
}
}
return true;
default:
return true;
}
});
LayoutParams param = new GridLayout.LayoutParams();
param.width = 0;
param.height = 0;
param.setMargins(0, 0, 0, 0);
param.columnSpec = GridLayout.spec(col, GridLayout.FILL, 1.f);
param.rowSpec = GridLayout.spec(row, GridLayout.FILL, 1.f);
button.setLayoutParams(param);
addView(button);
}
}
}
public void setTermuxTerminalViewClient(TermuxTerminalViewClient termuxTerminalViewClient) {
this.mTermuxTerminalViewClient = termuxTerminalViewClient;
}
}

View File

@@ -4,7 +4,7 @@ import android.app.Notification;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.os.Environment;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@@ -20,6 +20,7 @@ import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
import com.termux.shared.settings.preferences.TermuxPreferenceConstants; import com.termux.shared.settings.preferences.TermuxPreferenceConstants;
import com.termux.shared.data.DataUtils; import com.termux.shared.data.DataUtils;
import com.termux.shared.logger.Logger; import com.termux.shared.logger.Logger;
import com.termux.shared.termux.AndroidUtils;
import com.termux.shared.termux.TermuxUtils; import com.termux.shared.termux.TermuxUtils;
import com.termux.shared.termux.TermuxConstants; import com.termux.shared.termux.TermuxConstants;
@@ -86,7 +87,7 @@ public class CrashUtils {
Logger.logDebug(logTag, "A crash log file found at \"" + TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH + "\"."); Logger.logDebug(logTag, "A crash log file found at \"" + TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH + "\".");
sendCrashReportNotification(context, logTag, reportString, false); sendCrashReportNotification(context, logTag, reportString, false, false);
} }
}.start(); }.start();
} }
@@ -97,13 +98,15 @@ public class CrashUtils {
* *
* @param context The {@link Context} for operations. * @param context The {@link Context} for operations.
* @param logTag The log tag to use for logging. * @param logTag The log tag to use for logging.
* @param reportString The text for the crash report. * @param message The message for the crash report.
* @param forceNotification If set to {@code true}, then a notification will be shown * @param forceNotification If set to {@code true}, then a notification will be shown
* regardless of if pending intent is {@code null} or * regardless of if pending intent is {@code null} or
* {@link TermuxPreferenceConstants.TERMUX_APP#KEY_CRASH_REPORT_NOTIFICATIONS_ENABLED} * {@link TermuxPreferenceConstants.TERMUX_APP#KEY_CRASH_REPORT_NOTIFICATIONS_ENABLED}
* is {@code false}. * is {@code false}.
* @param addAppAndDeviceInfo If set to {@code true}, then app and device info will be appended
* to the message.
*/ */
public static void sendCrashReportNotification(final Context context, String logTag, String reportString, boolean forceNotification) { public static void sendCrashReportNotification(final Context context, String logTag, String message, boolean forceNotification, boolean addAppAndDeviceInfo) {
if (context == null) return; if (context == null) return;
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context); TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
@@ -121,18 +124,40 @@ public class CrashUtils {
Logger.logDebug(logTag, "Sending \"" + title + "\" notification."); Logger.logDebug(logTag, "Sending \"" + title + "\" notification.");
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.CRASH_REPORT.getName(), logTag, title, null, reportString, "\n\n" + TermuxUtils.getReportIssueMarkdownString(context), true)); StringBuilder reportString = new StringBuilder(message);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
if (addAppAndDeviceInfo) {
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true));
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
}
String userActionName = UserAction.CRASH_REPORT.getName();
ReportActivity.NewInstanceResult result = ReportActivity.newInstance(context, new ReportInfo(userActionName,
logTag, title, null, reportString.toString(),
"\n\n" + TermuxUtils.getReportIssueMarkdownString(context), true,
userActionName,
Environment.getExternalStorageDirectory() + "/" +
FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true)));
if (result.contentIntent == null) return;
// Must ensure result code for PendingIntents and id for notification are unique otherwise will override previous
int nextNotificationId = TermuxNotificationUtils.getNextNotificationId(context);
PendingIntent contentIntent = PendingIntent.getActivity(context, nextNotificationId, result.contentIntent, PendingIntent.FLAG_UPDATE_CURRENT);
PendingIntent deleteIntent = null;
if (result.deleteIntent != null)
deleteIntent = PendingIntent.getBroadcast(context, nextNotificationId, result.deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT);
// Setup the notification channel if not already set up // Setup the notification channel if not already set up
setupCrashReportsNotificationChannel(context); setupCrashReportsNotificationChannel(context);
// Build the notification // Build the notification
Notification.Builder builder = getCrashReportsNotificationBuilder(context, title, null, null, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE); Notification.Builder builder = getCrashReportsNotificationBuilder(context, title, null,
null, contentIntent, deleteIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
if (builder == null) return; if (builder == null) return;
// Send the notification // Send the notification
int nextNotificationId = TermuxNotificationUtils.getNextNotificationId(context);
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context); NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
if (notificationManager != null) if (notificationManager != null)
notificationManager.notify(nextNotificationId, builder.build()); notificationManager.notify(nextNotificationId, builder.build());
@@ -146,16 +171,17 @@ public class CrashUtils {
* @param title The title for the notification. * @param title The title for the notification.
* @param notificationText The second line text of the notification. * @param notificationText The second line text of the notification.
* @param notificationBigText The full text of the notification that may optionally be styled. * @param notificationBigText The full text of the notification that may optionally be styled.
* @param pendingIntent The {@link PendingIntent} which should be sent when notification is clicked. * @param contentIntent The {@link PendingIntent} which should be sent when notification is clicked.
* @param deleteIntent The {@link PendingIntent} which should be sent when notification is deleted.
* @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}. * @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}.
* @return Returns the {@link Notification.Builder}. * @return Returns the {@link Notification.Builder}.
*/ */
@Nullable @Nullable
public static Notification.Builder getCrashReportsNotificationBuilder(final Context context, final CharSequence title, final CharSequence notificationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) { public static Notification.Builder getCrashReportsNotificationBuilder(final Context context, final CharSequence title, final CharSequence notificationText, final CharSequence notificationBigText, final PendingIntent contentIntent, final PendingIntent deleteIntent, final int notificationMode) {
Notification.Builder builder = NotificationUtils.geNotificationBuilder(context, Notification.Builder builder = NotificationUtils.geNotificationBuilder(context,
TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_HIGH, TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_HIGH,
title, notificationText, notificationBigText, pendingIntent, notificationMode); title, notificationText, notificationBigText, contentIntent, deleteIntent, notificationMode);
if (builder == null) return null; if (builder == null) return null;

View File

@@ -4,12 +4,13 @@ import android.app.Notification;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.os.Environment;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.termux.R; import com.termux.R;
import com.termux.shared.activities.ReportActivity; import com.termux.shared.activities.ReportActivity;
import com.termux.shared.file.FileUtils;
import com.termux.shared.file.TermuxFileUtils; import com.termux.shared.file.TermuxFileUtils;
import com.termux.shared.models.ResultConfig; import com.termux.shared.models.ResultConfig;
import com.termux.shared.models.ResultData; import com.termux.shared.models.ResultData;
@@ -64,9 +65,12 @@ public class PluginUtils {
} }
boolean isPluginExecutionCommandWithPendingResult = executionCommand.isPluginExecutionCommandWithPendingResult(); boolean isPluginExecutionCommandWithPendingResult = executionCommand.isPluginExecutionCommandWithPendingResult();
boolean isExecutionCommandLoggingEnabled = Logger.shouldEnableLoggingForCustomLogLevel(executionCommand.backgroundCustomLogLevel);
// Log the output. ResultData should not be logged if pending result since ResultSender will do it // Log the output. ResultData should not be logged if pending result since ResultSender will do it
Logger.logDebugExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, !isPluginExecutionCommandWithPendingResult)); // or if logging is disabled
Logger.logDebugExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true,
!isPluginExecutionCommandWithPendingResult, isExecutionCommandLoggingEnabled));
// If execution command was started by a plugin which expects the result back // If execution command was started by a plugin which expects the result back
if (isPluginExecutionCommandWithPendingResult) { if (isPluginExecutionCommandWithPendingResult) {
@@ -77,11 +81,12 @@ public class PluginUtils {
setPluginResultDirectoryVariables(executionCommand); setPluginResultDirectoryVariables(executionCommand);
// Send result to caller // Send result to caller
error = ResultSender.sendCommandResultData(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.resultConfig, executionCommand.resultData); error = ResultSender.sendCommandResultData(context, logTag, executionCommand.getCommandIdAndLabelLogString(),
executionCommand.resultConfig, executionCommand.resultData, isExecutionCommandLoggingEnabled);
if (error != null) { if (error != null) {
// error will be added to existing Errors // error will be added to existing Errors
resultData.setStateFailed(error); resultData.setStateFailed(error);
Logger.logDebugExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, true)); Logger.logDebugExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, true, isExecutionCommandLoggingEnabled));
// Flash and send notification for the error // Flash and send notification for the error
Logger.showToast(context, ResultData.getErrorsListMinimalString(resultData), true); Logger.showToast(context, ResultData.getErrorsListMinimalString(resultData), true);
@@ -132,9 +137,11 @@ public class PluginUtils {
} }
boolean isPluginExecutionCommandWithPendingResult = executionCommand.isPluginExecutionCommandWithPendingResult(); boolean isPluginExecutionCommandWithPendingResult = executionCommand.isPluginExecutionCommandWithPendingResult();
boolean isExecutionCommandLoggingEnabled = Logger.shouldEnableLoggingForCustomLogLevel(executionCommand.backgroundCustomLogLevel);
// Log the error and any exception. ResultData should not be logged if pending result since ResultSender will do it // Log the error and any exception. ResultData should not be logged if pending result since ResultSender will do it
Logger.logErrorExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, !isPluginExecutionCommandWithPendingResult)); Logger.logErrorExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true,
!isPluginExecutionCommandWithPendingResult, isExecutionCommandLoggingEnabled));
// If execution command was started by a plugin which expects the result back // If execution command was started by a plugin which expects the result back
if (isPluginExecutionCommandWithPendingResult) { if (isPluginExecutionCommandWithPendingResult) {
@@ -145,11 +152,12 @@ public class PluginUtils {
setPluginResultDirectoryVariables(executionCommand); setPluginResultDirectoryVariables(executionCommand);
// Send result to caller // Send result to caller
error = ResultSender.sendCommandResultData(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.resultConfig, executionCommand.resultData); error = ResultSender.sendCommandResultData(context, logTag, executionCommand.getCommandIdAndLabelLogString(),
executionCommand.resultConfig, executionCommand.resultData, isExecutionCommandLoggingEnabled);
if (error != null) { if (error != null) {
// error will be added to existing Errors // error will be added to existing Errors
resultData.setStateFailed(error); resultData.setStateFailed(error);
Logger.logErrorExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, true)); Logger.logErrorExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, true, isExecutionCommandLoggingEnabled));
forceNotification = true; forceNotification = true;
} }
@@ -170,7 +178,7 @@ public class PluginUtils {
} }
/** Set variables which will be used by {@link ResultSender#sendCommandResultData(Context, String, String, ResultConfig, ResultData)} /** Set variables which will be used by {@link ResultSender#sendCommandResultData(Context, String, String, ResultConfig, ResultData, boolean)}
* to send back the result via {@link ResultConfig#resultPendingIntent}. */ * to send back the result via {@link ResultConfig#resultPendingIntent}. */
public static void setPluginResultPendingIntentVariables(ExecutionCommand executionCommand) { public static void setPluginResultPendingIntentVariables(ExecutionCommand executionCommand) {
ResultConfig resultConfig = executionCommand.resultConfig; ResultConfig resultConfig = executionCommand.resultConfig;
@@ -185,7 +193,7 @@ public class PluginUtils {
resultConfig.resultErrmsgKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG; resultConfig.resultErrmsgKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG;
} }
/** Set variables which will be used by {@link ResultSender#sendCommandResultData(Context, String, String, ResultConfig, ResultData)} /** Set variables which will be used by {@link ResultSender#sendCommandResultData(Context, String, String, ResultConfig, ResultData, boolean)}
* to send back the result by writing it to files in {@link ResultConfig#resultDirectoryPath}. */ * to send back the result by writing it to files in {@link ResultConfig#resultDirectoryPath}. */
public static void setPluginResultDirectoryVariables(ExecutionCommand executionCommand) { public static void setPluginResultDirectoryVariables(ExecutionCommand executionCommand) {
ResultConfig resultConfig = executionCommand.resultConfig; ResultConfig resultConfig = executionCommand.resultConfig;
@@ -219,8 +227,23 @@ public class PluginUtils {
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true)); reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true));
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context)); reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.PLUGIN_EXECUTION_COMMAND.getName(), logTag, title, null, reportString.toString(), null,true)); String userActionName = UserAction.PLUGIN_EXECUTION_COMMAND.getName();
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); ReportActivity.NewInstanceResult result = ReportActivity.newInstance(context,
new ReportInfo(userActionName, logTag, title, null,
reportString.toString(), null,true,
userActionName,
Environment.getExternalStorageDirectory() + "/" +
FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true)));
if (result.contentIntent == null) return;
// Must ensure result code for PendingIntents and id for notification are unique otherwise will override previous
int nextNotificationId = TermuxNotificationUtils.getNextNotificationId(context);
PendingIntent contentIntent = PendingIntent.getActivity(context, nextNotificationId, result.contentIntent, PendingIntent.FLAG_UPDATE_CURRENT);
PendingIntent deleteIntent = null;
if (result.deleteIntent != null)
deleteIntent = PendingIntent.getBroadcast(context, nextNotificationId, result.deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT);
// Setup the notification channel if not already set up // Setup the notification channel if not already set up
setupPluginCommandErrorsNotificationChannel(context); setupPluginCommandErrorsNotificationChannel(context);
@@ -230,11 +253,11 @@ public class PluginUtils {
//CharSequence notificationTextCharSequence = notificationTextString; //CharSequence notificationTextCharSequence = notificationTextString;
// Build the notification // Build the notification
Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(context, title, notificationTextCharSequence, notificationTextCharSequence, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE); Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(context, title,
notificationTextCharSequence, notificationTextCharSequence, contentIntent, deleteIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
if (builder == null) return; if (builder == null) return;
// Send the notification // Send the notification
int nextNotificationId = TermuxNotificationUtils.getNextNotificationId(context);
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context); NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
if (notificationManager != null) if (notificationManager != null)
notificationManager.notify(nextNotificationId, builder.build()); notificationManager.notify(nextNotificationId, builder.build());
@@ -248,16 +271,19 @@ public class PluginUtils {
* @param title The title for the notification. * @param title The title for the notification.
* @param notificationText The second line text of the notification. * @param notificationText The second line text of the notification.
* @param notificationBigText The full text of the notification that may optionally be styled. * @param notificationBigText The full text of the notification that may optionally be styled.
* @param pendingIntent The {@link PendingIntent} which should be sent when notification is clicked. * @param contentIntent The {@link PendingIntent} which should be sent when notification is clicked.
* @param deleteIntent The {@link PendingIntent} which should be sent when notification is deleted.
* @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}. * @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}.
* @return Returns the {@link Notification.Builder}. * @return Returns the {@link Notification.Builder}.
*/ */
@Nullable @Nullable
public static Notification.Builder getPluginCommandErrorsNotificationBuilder(final Context context, final CharSequence title, final CharSequence notificationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) { public static Notification.Builder getPluginCommandErrorsNotificationBuilder(
final Context context, final CharSequence title, final CharSequence notificationText,
final CharSequence notificationBigText, final PendingIntent contentIntent, final PendingIntent deleteIntent, final int notificationMode) {
Notification.Builder builder = NotificationUtils.geNotificationBuilder(context, Notification.Builder builder = NotificationUtils.geNotificationBuilder(context,
TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_HIGH, TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_HIGH,
title, notificationText, notificationBigText, pendingIntent, notificationMode); title, notificationText, notificationBigText, contentIntent, deleteIntent, notificationMode);
if (builder == null) return null; if (builder == null) return null;
@@ -295,10 +321,12 @@ public class PluginUtils {
* @param context The {@link Context} to get error string. * @param context The {@link Context} to get error string.
* @return Returns the {@code error} if policy is violated, otherwise {@code null}. * @return Returns the {@code error} if policy is violated, otherwise {@code null}.
*/ */
public static String checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(final Context context) { public static String checkIfAllowExternalAppsPolicyIsViolated(final Context context, String apiName) {
String errmsg = null; String errmsg = null;
if (!SharedProperties.isPropertyValueTrue(context, TermuxPropertyConstants.getTermuxPropertiesFile(), TermuxConstants.PROP_ALLOW_EXTERNAL_APPS, true)) { if (!SharedProperties.isPropertyValueTrue(context, TermuxPropertyConstants.getTermuxPropertiesFile(),
errmsg = context.getString(R.string.error_run_command_service_allow_external_apps_ungranted); TermuxConstants.PROP_ALLOW_EXTERNAL_APPS, true)) {
errmsg = context.getString(R.string.error_allow_external_apps_ungranted, apiName,
TermuxFileUtils.getUnExpandedTermuxPath(TermuxConstants.TERMUX_PROPERTIES_PRIMARY_FILE_PATH));
} }
return errmsg; return errmsg;

View File

@@ -1,7 +1,6 @@
package com.termux.filepicker; package com.termux.filepicker;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
@@ -9,6 +8,9 @@ import android.provider.OpenableColumns;
import android.util.Patterns; import android.util.Patterns;
import com.termux.R; import com.termux.R;
import com.termux.shared.data.DataUtils;
import com.termux.shared.data.IntentUtils;
import com.termux.shared.interact.MessageDialogUtils;
import com.termux.shared.interact.TextInputDialogUtils; import com.termux.shared.interact.TextInputDialogUtils;
import com.termux.shared.termux.TermuxConstants; import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE; import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
@@ -39,6 +41,8 @@ public class TermuxFileReceiverActivity extends Activity {
*/ */
boolean mFinishOnDismissNameDialog = true; boolean mFinishOnDismissNameDialog = true;
private static final String API_TAG = TermuxConstants.TERMUX_APP_NAME + "FileReceiver";
private static final String LOG_TAG = "TermuxFileReceiverActivity"; private static final String LOG_TAG = "TermuxFileReceiverActivity";
static boolean isSharedTextAnUrl(String sharedText) { static boolean isSharedTextAnUrl(String sharedText) {
@@ -55,44 +59,66 @@ public class TermuxFileReceiverActivity extends Activity {
final String type = intent.getType(); final String type = intent.getType();
final String scheme = intent.getScheme(); final String scheme = intent.getScheme();
Logger.logVerbose(LOG_TAG, "Intent Received:\n" + IntentUtils.getIntentString(intent));
final String sharedTitle = IntentUtils.getStringExtraIfSet(intent, Intent.EXTRA_TITLE, null);
if (Intent.ACTION_SEND.equals(action) && type != null) { if (Intent.ACTION_SEND.equals(action) && type != null) {
final String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT); final String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
final Uri sharedUri = intent.getParcelableExtra(Intent.EXTRA_STREAM); final Uri sharedUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
if (sharedText != null) { if (sharedUri != null) {
handleContentUri(sharedUri, sharedTitle);
} else if (sharedText != null) {
if (isSharedTextAnUrl(sharedText)) { if (isSharedTextAnUrl(sharedText)) {
handleUrlAndFinish(sharedText); handleUrlAndFinish(sharedText);
} else { } else {
String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT); String subject = IntentUtils.getStringExtraIfSet(intent, Intent.EXTRA_SUBJECT, null);
if (subject == null) subject = intent.getStringExtra(Intent.EXTRA_TITLE); if (subject == null) subject = sharedTitle;
if (subject != null) subject += ".txt"; if (subject != null) subject += ".txt";
promptNameAndSave(new ByteArrayInputStream(sharedText.getBytes(StandardCharsets.UTF_8)), subject); promptNameAndSave(new ByteArrayInputStream(sharedText.getBytes(StandardCharsets.UTF_8)), subject);
} }
} else if (sharedUri != null) {
handleContentUri(sharedUri, intent.getStringExtra(Intent.EXTRA_TITLE));
} else { } else {
showErrorDialogAndQuit("Send action without content - nothing to save."); showErrorDialogAndQuit("Send action without content - nothing to save.");
} }
} else if ("content".equals(scheme)) {
handleContentUri(intent.getData(), intent.getStringExtra(Intent.EXTRA_TITLE));
} else if ("file".equals(scheme)) {
// When e.g. clicking on a downloaded apk:
String path = intent.getData().getPath();
File file = new File(path);
try {
FileInputStream in = new FileInputStream(file);
promptNameAndSave(in, file.getName());
} catch (FileNotFoundException e) {
showErrorDialogAndQuit("Cannot open file: " + e.getMessage() + ".");
}
} else { } else {
showErrorDialogAndQuit("Unable to receive any file or URL."); Uri dataUri = intent.getData();
if (dataUri == null) {
showErrorDialogAndQuit("Data uri not passed.");
return;
}
if ("content".equals(scheme)) {
handleContentUri(dataUri, sharedTitle);
} else if ("file".equals(scheme)) {
// When e.g. clicking on a downloaded apk:
String path = dataUri.getPath();
if (DataUtils.isNullOrEmpty(path)) {
showErrorDialogAndQuit("File path from data uri is null, empty or invalid.");
return;
}
File file = new File(path);
try {
FileInputStream in = new FileInputStream(file);
promptNameAndSave(in, file.getName());
} catch (FileNotFoundException e) {
showErrorDialogAndQuit("Cannot open file: " + e.getMessage() + ".");
}
} else {
showErrorDialogAndQuit("Unable to receive any file or URL.");
}
} }
} }
void showErrorDialogAndQuit(String message) { void showErrorDialogAndQuit(String message) {
mFinishOnDismissNameDialog = false; mFinishOnDismissNameDialog = false;
new AlertDialog.Builder(this).setMessage(message).setOnDismissListener(dialog -> finish()).setPositiveButton(android.R.string.ok, (dialog, which) -> finish()).show(); MessageDialogUtils.showMessage(this,
API_TAG, message,
null, (dialog, which) -> finish(),
null, null,
dialog -> finish());
} }
void handleContentUri(final Uri uri, String subjectFromIntent) { void handleContentUri(final Uri uri, String subjectFromIntent) {
@@ -157,10 +183,17 @@ public class TermuxFileReceiverActivity extends Activity {
public File saveStreamWithName(InputStream in, String attachmentFileName) { public File saveStreamWithName(InputStream in, String attachmentFileName) {
File receiveDir = new File(TERMUX_RECEIVEDIR); File receiveDir = new File(TERMUX_RECEIVEDIR);
if (DataUtils.isNullOrEmpty(attachmentFileName)) {
showErrorDialogAndQuit("File name cannot be null or empty");
return null;
}
if (!receiveDir.isDirectory() && !receiveDir.mkdirs()) { if (!receiveDir.isDirectory() && !receiveDir.mkdirs()) {
showErrorDialogAndQuit("Cannot create directory: " + receiveDir.getAbsolutePath()); showErrorDialogAndQuit("Cannot create directory: " + receiveDir.getAbsolutePath());
return null; return null;
} }
try { try {
final File outFile = new File(receiveDir, attachmentFileName); final File outFile = new File(receiveDir, attachmentFileName);
try (FileOutputStream f = new FileOutputStream(outFile)) { try (FileOutputStream f = new FileOutputStream(outFile)) {
@@ -182,7 +215,7 @@ public class TermuxFileReceiverActivity extends Activity {
final File urlOpenerProgramFile = new File(URL_OPENER_PROGRAM); final File urlOpenerProgramFile = new File(URL_OPENER_PROGRAM);
if (!urlOpenerProgramFile.isFile()) { if (!urlOpenerProgramFile.isFile()) {
showErrorDialogAndQuit("The following file does not exist:\n$HOME/bin/termux-url-opener\n\n" showErrorDialogAndQuit("The following file does not exist:\n$HOME/bin/termux-url-opener\n\n"
+ "Create this file as a script or a symlink - it will be called with the shared URL as only argument."); + "Create this file as a script or a symlink - it will be called with the shared URL as the first argument.");
return; return;
} }

View File

@@ -1,4 +1,6 @@
<com.termux.app.terminal.TermuxActivityRootView xmlns:android="http://schemas.android.com/apk/res/android" <com.termux.app.terminal.TermuxActivityRootView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_termux_root_view" android:id="@+id/activity_termux_root_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@@ -6,9 +8,12 @@
android:fitsSystemWindows="true"> android:fitsSystemWindows="true">
<RelativeLayout <RelativeLayout
android:id="@+id/activity_termux_root_relative_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" android:layout_weight="1"
android:layout_marginHorizontal="3dp"
android:layout_marginVertical="0dp"
android:orientation="vertical"> android:orientation="vertical">
<androidx.drawerlayout.widget.DrawerLayout <androidx.drawerlayout.widget.DrawerLayout
@@ -22,13 +27,13 @@
android:id="@+id/terminal_view" android:id="@+id/terminal_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginRight="3dp" android:defaultFocusHighlightEnabled="false"
android:layout_marginLeft="3dp"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape" android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
android:scrollbars="vertical" android:scrollbars="vertical"
android:importantForAutofill="no" android:importantForAutofill="no"
android:autofillHints="password" /> android:autofillHints="password"
tools:ignore="UnusedAttribute" />
<LinearLayout <LinearLayout
android:id="@+id/left_drawer" android:id="@+id/left_drawer"
@@ -95,7 +100,7 @@
android:visibility="gone" android:visibility="gone"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="37.5dp" android:layout_height="37.5dp"
android:background="@android:drawable/screen_background_dark_transparent" android:background="@color/black"
android:layout_alignParentBottom="true" /> android:layout_alignParentBottom="true" />
</RelativeLayout> </RelativeLayout>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<com.termux.app.terminal.io.extrakeys.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android" <com.termux.shared.terminal.io.extrakeys.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/terminal_toolbar_extra_keys" android:id="@+id/terminal_toolbar_extra_keys"
style="?android:attr/buttonBarStyle" style="?android:attr/buttonBarStyle"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -9,10 +9,10 @@
<!ENTITY TERMUX_STYLING_APP_NAME "Termux:Styling"> <!ENTITY TERMUX_STYLING_APP_NAME "Termux:Styling">
<!ENTITY TERMUX_TASKER_APP_NAME "Termux:Tasker"> <!ENTITY TERMUX_TASKER_APP_NAME "Termux:Tasker">
<!ENTITY TERMUX_WIDGET_APP_NAME "Termux:Widget"> <!ENTITY TERMUX_WIDGET_APP_NAME "Termux:Widget">
<!ENTITY TERMUX_PROPERTIES_PRIMARY_PATH_SHORT "~/.termux/termux.properties">
]> ]>
<resources> <resources>
<string name="application_name">&TERMUX_APP_NAME;</string> <string name="application_name">&TERMUX_APP_NAME;</string>
<string name="shared_user_label">&TERMUX_APP_NAME; user</string> <string name="shared_user_label">&TERMUX_APP_NAME; user</string>
@@ -21,7 +21,7 @@
<!-- Termux RUN_COMMAND permission --> <!-- Termux RUN_COMMAND permission -->
<string name="permission_run_command_label">Run commands in &TERMUX_APP_NAME; environment</string> <string name="permission_run_command_label">Run commands in &TERMUX_APP_NAME; environment</string>
<string name="permission_run_command_description">execute arbitrary commands within &TERMUX_APP_NAME; <string name="permission_run_command_description">execute arbitrary commands within &TERMUX_APP_NAME;
environment</string> environment and access files</string>
@@ -31,7 +31,9 @@
<string name="bootstrap_error_body">&TERMUX_APP_NAME; was unable to install the bootstrap packages.</string> <string name="bootstrap_error_body">&TERMUX_APP_NAME; was unable to install the bootstrap packages.</string>
<string name="bootstrap_error_abort">Abort</string> <string name="bootstrap_error_abort">Abort</string>
<string name="bootstrap_error_try_again">Try again</string> <string name="bootstrap_error_try_again">Try again</string>
<string name="bootstrap_error_not_primary_user_message">&TERMUX_APP_NAME; can only be run as the primary user.\nBootstrap binaries compiled for &TERMUX_APP_NAME; have hardcoded $PREFIX path and cannot be installed under any path other than \"%1$s\".</string> <string name="bootstrap_error_not_primary_user_message">&TERMUX_APP_NAME; can only be run as the primary user.
\nBootstrap binaries compiled for &TERMUX_APP_NAME; have hardcoded $PREFIX path and cannot be installed
under any path other than %1$s.</string>
@@ -78,6 +80,7 @@
<string name="action_report_issue">Report Issue</string> <string name="action_report_issue">Report Issue</string>
<string name="msg_generating_report">Generating Report</string> <string name="msg_generating_report">Generating Report</string>
<string name="msg_add_termux_debug_info">Add termux debug info to report?</string>
<string name="error_styling_not_installed">The &TERMUX_STYLING_APP_NAME; Plugin App is not installed.</string> <string name="error_styling_not_installed">The &TERMUX_STYLING_APP_NAME; Plugin App is not installed.</string>
<string name="action_styling_install">Install</string> <string name="action_styling_install">Install</string>
@@ -92,24 +95,19 @@
<!-- TermuxService --> <!-- TermuxService -->
<string name="error_display_over_other_apps_permission_not_granted">&TERMUX_APP_NAME; requires \"Display over other apps\" permission to start terminal sessions from background on Android >= 10. Grants it from Settings -> Apps -> &TERMUX_APP_NAME; -> Advanced</string> <string name="error_display_over_other_apps_permission_not_granted">&TERMUX_APP_NAME; requires
\"Display over other apps\" permission to start terminal sessions from background on Android >= 10.
Grants it from Settings -> Apps -> &TERMUX_APP_NAME; -> Advanced</string>
<!-- Termux RunCommandService --> <!-- Termux RunCommandService -->
<string name="error_run_command_service_invalid_intent_action">Invalid intent action to RunCommandService: `%1$s`</string> <string name="error_run_command_service_invalid_intent_action">Invalid intent action to RunCommandService: `%1$s`</string>
<string name="error_run_command_service_mandatory_extra_missing">Mandatory extra missing to RunCommandService: \"%1$s\"</string> <string name="error_run_command_service_mandatory_extra_missing">Mandatory extra missing to RunCommandService: \"%1$s\"</string>
<string name="error_run_command_service_allow_external_apps_ungranted">RunCommandService require `allow-external-apps` property to be set to `true` in `&TERMUX_PROPERTIES_PRIMARY_PATH_SHORT;` file.</string>
<string name="error_run_command_service_api_help">Visit %1$s for more info on RUN_COMMAND Intent usage.</string> <string name="error_run_command_service_api_help">Visit %1$s for more info on RUN_COMMAND Intent usage.</string>
<!-- Termux Execution Commands -->
<string name="msg_executable_absolute_path">Executable Absolute Path: \"%1$s\"</string>
<string name="msg_working_directory_absolute_path">Working Directory Absolute Path: \"%1$s\"</string>
<!-- Termux File Receiver --> <!-- Termux File Receiver -->
<string name="title_file_received">Save file in ~/downloads/</string> <string name="title_file_received">Save file in ~/downloads/</string>
<string name="action_file_received_edit">Edit</string> <string name="action_file_received_edit">Edit</string>
@@ -117,6 +115,12 @@
<!-- Miscellaneous -->
<string name="error_allow_external_apps_ungranted">%1$s requires `allow-external-apps`
property to be set to `true` in `%2$s` file.</string>
<!-- Termux Settings --> <!-- Termux Settings -->
<string name="title_activity_termux_settings">&TERMUX_APP_NAME; Settings</string> <string name="title_activity_termux_settings">&TERMUX_APP_NAME; Settings</string>
@@ -137,7 +141,8 @@
<!-- Terminal View Key Logging --> <!-- Terminal View Key Logging -->
<string name="termux_terminal_view_key_logging_enabled_title">Terminal View Key Logging</string> <string name="termux_terminal_view_key_logging_enabled_title">Terminal View Key Logging</string>
<string name="termux_terminal_view_key_logging_enabled_off">Logs will not have entries for terminal view keys. (Default)</string> <string name="termux_terminal_view_key_logging_enabled_off">Logs will not have entries for terminal view keys. (Default)</string>
<string name="termux_terminal_view_key_logging_enabled_on">Logcat logs will have entries for terminal view keys. These are very verbose and should be disabled under normal circumstances or will cause performance issues.</string> <string name="termux_terminal_view_key_logging_enabled_on">Logcat logs will have entries for terminal view keys.
These are very verbose and should be disabled under normal circumstances or will cause performance issues.</string>
<!-- Plugin Error Notifications --> <!-- Plugin Error Notifications -->
<string name="termux_plugin_error_notifications_enabled_title">Plugin Error Notifications</string> <string name="termux_plugin_error_notifications_enabled_title">Plugin Error Notifications</string>
@@ -164,15 +169,52 @@
<!-- Soft Keyboard Only If No Hardware--> <!-- Soft Keyboard Only If No Hardware-->
<string name="termux_soft_keyboard_enabled_only_if_no_hardware_title">Soft Keyboard Only If No Hardware</string> <string name="termux_soft_keyboard_enabled_only_if_no_hardware_title">Soft Keyboard Only If No Hardware</string>
<string name="termux_soft_keyboard_enabled_only_if_no_hardware_off">Soft keyboard will be enabled even if hardware keyboard is connected. (Default)</string> <string name="termux_soft_keyboard_enabled_only_if_no_hardware_off">Soft keyboard will be enabled even if
<string name="termux_soft_keyboard_enabled_only_if_no_hardware_on">Soft keyboard will be enabled only if no hardware keyboard is connected.</string> hardware keyboard is connected. (Default)</string>
<string name="termux_soft_keyboard_enabled_only_if_no_hardware_on">Soft keyboard will be enabled only if
no hardware keyboard is connected.</string>
<!-- Termux Tasker App Preferences --> <!-- Terminal View Preferences -->
<string name="termux_terminal_view_preferences_title">Terminal View</string>
<string name="termux_terminal_view_preferences_summary">Preferences for terminal view</string>
<!-- View Category -->
<string name="termux_terminal_view_view_header">View</string>
<!-- Terminal View Margin Adjustment -->
<string name="termux_terminal_view_terminal_margin_adjustment_title">Terminal Margin Adjustment</string>
<string name="termux_terminal_view_terminal_margin_adjustment_off">Terminal margin adjustment will be disabled.</string>
<string name="termux_terminal_view_terminal_margin_adjustment_on">Terminal margin adjustment will be enabled.
It should be enabled to try to fix the issue where soft keyboard covers part of extra keys/terminal view.
If it causes screen flickering on your devices, then disable it. (Default)</string>
<!-- Termux:API App Preferences -->
<string name="termux_api_preferences_title">&TERMUX_API_APP_NAME;</string>
<string name="termux_api_preferences_summary">Preferences for &TERMUX_API_APP_NAME; app</string>
<!-- Termux:Float App Preferences -->
<string name="termux_float_preferences_title">&TERMUX_FLOAT_APP_NAME;</string>
<string name="termux_float_preferences_summary">Preferences for &TERMUX_FLOAT_APP_NAME; app</string>
<!-- Termux:Tasker App Preferences -->
<string name="termux_tasker_preferences_title">&TERMUX_TASKER_APP_NAME;</string> <string name="termux_tasker_preferences_title">&TERMUX_TASKER_APP_NAME;</string>
<string name="termux_tasker_preferences_summary">Preferences for &TERMUX_TASKER_APP_NAME; app</string> <string name="termux_tasker_preferences_summary">Preferences for &TERMUX_TASKER_APP_NAME; app</string>
<!-- Termux:Widget App Preferences -->
<string name="termux_widget_preferences_title">&TERMUX_WIDGET_APP_NAME;</string>
<string name="termux_widget_preferences_summary">Preferences for &TERMUX_WIDGET_APP_NAME; app</string>
<!-- About Preference --> <!-- About Preference -->
<string name="about_preference_title">About</string> <string name="about_preference_title">About</string>

View File

@@ -6,6 +6,20 @@
app:summary="@string/termux_preferences_summary" app:summary="@string/termux_preferences_summary"
app:fragment="com.termux.app.fragments.settings.TermuxPreferencesFragment"/> app:fragment="com.termux.app.fragments.settings.TermuxPreferencesFragment"/>
<Preference
app:key="termux_api"
app:title="@string/termux_api_preferences_title"
app:summary="@string/termux_api_preferences_summary"
app:isPreferenceVisible="false"
app:fragment="com.termux.app.fragments.settings.TermuxAPIPreferencesFragment"/>
<Preference
app:key="termux_float"
app:title="@string/termux_float_preferences_title"
app:summary="@string/termux_float_preferences_summary"
app:isPreferenceVisible="false"
app:fragment="com.termux.app.fragments.settings.TermuxFloatPreferencesFragment"/>
<Preference <Preference
app:key="termux_tasker" app:key="termux_tasker"
app:title="@string/termux_tasker_preferences_title" app:title="@string/termux_tasker_preferences_title"
@@ -13,6 +27,13 @@
app:isPreferenceVisible="false" app:isPreferenceVisible="false"
app:fragment="com.termux.app.fragments.settings.TermuxTaskerPreferencesFragment"/> app:fragment="com.termux.app.fragments.settings.TermuxTaskerPreferencesFragment"/>
<Preference
app:key="termux_widget"
app:title="@string/termux_widget_preferences_title"
app:summary="@string/termux_widget_preferences_summary"
app:isPreferenceVisible="false"
app:fragment="com.termux.app.fragments.settings.TermuxWidgetPreferencesFragment"/>
<Preference <Preference
app:key="about" app:key="about"
app:title="@string/about_preference_title" app:title="@string/about_preference_title"

View File

@@ -0,0 +1,15 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:key="logging"
app:title="@string/termux_logging_header">
<ListPreference
app:defaultValue="1"
app:key="log_level"
app:title="@string/termux_log_level_title"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
</PreferenceScreen>

View File

@@ -0,0 +1,8 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
app:title="@string/termux_debugging_preferences_title"
app:summary="@string/termux_debugging_preferences_summary"
app:fragment="com.termux.app.fragments.settings.termux_api.DebuggingPreferencesFragment"/>
</PreferenceScreen>

View File

@@ -0,0 +1,21 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:key="logging"
app:title="@string/termux_logging_header">
<ListPreference
app:defaultValue="1"
app:key="log_level"
app:title="@string/termux_log_level_title"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
app:key="terminal_view_key_logging_enabled"
app:summaryOff="@string/termux_terminal_view_key_logging_enabled_off"
app:summaryOn="@string/termux_terminal_view_key_logging_enabled_on"
app:title="@string/termux_terminal_view_key_logging_enabled_title" />
</PreferenceCategory>
</PreferenceScreen>

View File

@@ -0,0 +1,8 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
app:title="@string/termux_debugging_preferences_title"
app:summary="@string/termux_debugging_preferences_summary"
app:fragment="com.termux.app.fragments.settings.termux_float.DebuggingPreferencesFragment"/>
</PreferenceScreen>

View File

@@ -10,4 +10,9 @@
app:summary="@string/termux_terminal_io_preferences_summary" app:summary="@string/termux_terminal_io_preferences_summary"
app:fragment="com.termux.app.fragments.settings.termux.TerminalIOPreferencesFragment"/> app:fragment="com.termux.app.fragments.settings.termux.TerminalIOPreferencesFragment"/>
<Preference
app:title="@string/termux_terminal_view_preferences_title"
app:summary="@string/termux_terminal_view_preferences_summary"
app:fragment="com.termux.app.fragments.settings.termux.TerminalViewPreferencesFragment"/>
</PreferenceScreen> </PreferenceScreen>

View File

@@ -0,0 +1,15 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:key="view"
app:title="@string/termux_terminal_view_view_header">
<SwitchPreferenceCompat
app:key="terminal_margin_adjustment"
app:summaryOff="@string/termux_terminal_view_terminal_margin_adjustment_off"
app:summaryOn="@string/termux_terminal_view_terminal_margin_adjustment_on"
app:title="@string/termux_terminal_view_terminal_margin_adjustment_title" />
</PreferenceCategory>
</PreferenceScreen>

View File

@@ -0,0 +1,15 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:key="logging"
app:title="@string/termux_logging_header">
<ListPreference
app:defaultValue="1"
app:key="log_level"
app:title="@string/termux_log_level_title"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
</PreferenceScreen>

View File

@@ -0,0 +1,8 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
app:title="@string/termux_debugging_preferences_title"
app:summary="@string/termux_debugging_preferences_summary"
app:fragment="com.termux.app.fragments.settings.termux_widget.DebuggingPreferencesFragment"/>
</PreferenceScreen>

View File

@@ -4,7 +4,7 @@ buildscript {
google() google()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.2.1' classpath 'com.android.tools.build:gradle:4.2.2'
} }
} }

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

2
jitpack.yml Normal file
View File

@@ -0,0 +1,2 @@
env:
JITPACK_NDK_VERSION: "21.1.6352462"

View File

@@ -3,7 +3,7 @@ apply plugin: 'maven-publish'
android { android {
compileSdkVersion project.properties.compileSdkVersion.toInteger() compileSdkVersion project.properties.compileSdkVersion.toInteger()
ndkVersion project.properties.ndkVersion ndkVersion = System.getenv("JITPACK_NDK_VERSION") ?: project.properties.ndkVersion
defaultConfig { defaultConfig {
minSdkVersion project.properties.minSdkVersion.toInteger() minSdkVersion project.properties.minSdkVersion.toInteger()
@@ -66,7 +66,7 @@ afterEvaluate {
from components.release from components.release
groupId = 'com.termux' groupId = 'com.termux'
artifactId = 'terminal-emulator' artifactId = 'terminal-emulator'
version = '0.115' version = '0.118.0'
artifact(sourceJar) artifact(sourceJar)
} }
} }

View File

@@ -0,0 +1,80 @@
package com.termux.terminal;
import android.util.Log;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
public class Logger {
public static void logError(TerminalSessionClient client, String logTag, String message) {
if (client != null)
client.logError(logTag, message);
else
Log.e(logTag, message);
}
public static void logWarn(TerminalSessionClient client, String logTag, String message) {
if (client != null)
client.logWarn(logTag, message);
else
Log.w(logTag, message);
}
public static void logInfo(TerminalSessionClient client, String logTag, String message) {
if (client != null)
client.logInfo(logTag, message);
else
Log.i(logTag, message);
}
public static void logDebug(TerminalSessionClient client, String logTag, String message) {
if (client != null)
client.logDebug(logTag, message);
else
Log.d(logTag, message);
}
public static void logVerbose(TerminalSessionClient client, String logTag, String message) {
if (client != null)
client.logVerbose(logTag, message);
else
Log.v(logTag, message);
}
public static void logStackTraceWithMessage(TerminalSessionClient client, String tag, String message, Throwable throwable) {
logError(client, tag, getMessageAndStackTraceString(message, throwable));
}
public static String getMessageAndStackTraceString(String message, Throwable throwable) {
if (message == null && throwable == null)
return null;
else if (message != null && throwable != null)
return message + ":\n" + getStackTraceString(throwable);
else if (throwable == null)
return message;
else
return getStackTraceString(throwable);
}
public static String getStackTraceString(Throwable throwable) {
if (throwable == null) return null;
String stackTraceString = null;
try {
StringWriter errors = new StringWriter();
PrintWriter pw = new PrintWriter(errors);
throwable.printStackTrace(pw);
pw.close();
stackTraceString = errors.toString();
errors.close();
} catch (IOException e) {
e.printStackTrace();
}
return stackTraceString;
}
}

View File

@@ -54,7 +54,7 @@ public final class TerminalBuffer {
} }
public String getSelectedText(int selX1, int selY1, int selX2, int selY2, boolean joinBackLines) { public String getSelectedText(int selX1, int selY1, int selX2, int selY2, boolean joinBackLines) {
return getSelectedText(selX1, selY1, selX2, selY2, true, false); return getSelectedText(selX1, selY1, selX2, selY2, joinBackLines, false);
} }
public String getSelectedText(int selX1, int selY1, int selX2, int selY2, boolean joinBackLines, boolean joinFullLines) { public String getSelectedText(int selX1, int selY1, int selX2, int selY2, boolean joinBackLines, boolean joinFullLines) {
@@ -93,8 +93,11 @@ public final class TerminalBuffer {
if (c != ' ') lastPrintingCharIndex = i; if (c != ' ') lastPrintingCharIndex = i;
} }
} }
if (lastPrintingCharIndex != -1)
builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1); int len = lastPrintingCharIndex - x1Index + 1;
if (lastPrintingCharIndex != -1 && len > 0)
builder.append(line, x1Index, len);
boolean lineFillsWidth = lastPrintingCharIndex == x2Index - 1; boolean lineFillsWidth = lastPrintingCharIndex == x2Index - 1;
if ((!joinBackLines || !rowLineWrap) && (!joinFullLines || !lineFillsWidth) if ((!joinBackLines || !rowLineWrap) && (!joinFullLines || !lineFillsWidth)
&& row < selY2 && row < mScreenRows - 1) builder.append('\n'); && row < selY2 && row < mScreenRows - 1) builder.append('\n');
@@ -102,6 +105,45 @@ public final class TerminalBuffer {
return builder.toString(); return builder.toString();
} }
public String getWordAtLocation(int x, int y) {
// Set y1 and y2 to the lines where the wrapped line starts and ends.
// I.e. if a line that is wrapped to 3 lines starts at line 4, and this
// is called with y=5, then y1 would be set to 4 and y2 would be set to 6.
int y1 = y;
int y2 = y;
while (y1 > 0 && !getSelectedText(0, y1 - 1, mColumns, y, true, true).contains("\n")) {
y1--;
}
while (y2 < mScreenRows && !getSelectedText(0, y, mColumns, y2 + 1, true, true).contains("\n")) {
y2++;
}
// Get the text for the whole wrapped line
String text = getSelectedText(0, y1, mColumns, y2, true, true);
// The index of x in text
int textOffset = (y - y1) * mColumns + x;
if (textOffset >= text.length()) {
// The click was to the right of the last word on the line, so
// there's no word to return
return "";
}
// Set x1 and x2 to the indices of the last space before x and the
// first space after x in text respectively
int x1 = text.lastIndexOf(' ', textOffset);
int x2 = text.indexOf(' ', textOffset);
if (x2 == -1) {
x2 = text.length();
}
if (x1 == x2) {
// The click was on a space, so there's no word to return
return "";
}
return text.substring(x1 + 1, x2);
}
public int getActiveTranscriptRows() { public int getActiveTranscriptRows() {
return mActiveTranscriptRows; return mActiveTranscriptRows;
} }
@@ -407,8 +449,8 @@ public final class TerminalBuffer {
} }
public void setChar(int column, int row, int codePoint, long style) { public void setChar(int column, int row, int codePoint, long style) {
if (row >= mScreenRows || column >= mColumns) if (row < 0 || row >= mScreenRows || column < 0 || column >= mColumns)
throw new IllegalArgumentException("row=" + row + ", column=" + column + ", mScreenRows=" + mScreenRows + ", mColumns=" + mColumns); throw new IllegalArgumentException("TerminalBuffer.setChar(): row=" + row + ", column=" + column + ", mScreenRows=" + mScreenRows + ", mColumns=" + mColumns);
row = externalToInternalRow(row); row = externalToInternalRow(row);
allocateFullLineIfNecessary(row).setChar(column, codePoint, style); allocateFullLineIfNecessary(row).setChar(column, codePoint, style);
} }

View File

@@ -57,7 +57,7 @@ public final class TerminalColorScheme {
0xff808080, 0xff8a8a8a, 0xff949494, 0xff9e9e9e, 0xffa8a8a8, 0xffb2b2b2, 0xffbcbcbc, 0xffc6c6c6, 0xffd0d0d0, 0xffdadada, 0xffe4e4e4, 0xffeeeeee, 0xff808080, 0xff8a8a8a, 0xff949494, 0xff9e9e9e, 0xffa8a8a8, 0xffb2b2b2, 0xffbcbcbc, 0xffc6c6c6, 0xffd0d0d0, 0xffdadada, 0xffe4e4e4, 0xffeeeeee,
// COLOR_INDEX_DEFAULT_FOREGROUND, COLOR_INDEX_DEFAULT_BACKGROUND and COLOR_INDEX_DEFAULT_CURSOR: // COLOR_INDEX_DEFAULT_FOREGROUND, COLOR_INDEX_DEFAULT_BACKGROUND and COLOR_INDEX_DEFAULT_CURSOR:
0xffffffff, 0xff000000, 0xffA9AAA9}; 0xffffffff, 0xff000000, 0xffffffff};
public final int[] mDefaultColors = new int[TextStyle.NUM_INDEXED_COLORS]; public final int[] mDefaultColors = new int[TextStyle.NUM_INDEXED_COLORS];
@@ -71,6 +71,7 @@ public final class TerminalColorScheme {
public void updateWith(Properties props) { public void updateWith(Properties props) {
reset(); reset();
boolean cursorPropExists = false;
for (Map.Entry<Object, Object> entries : props.entrySet()) { for (Map.Entry<Object, Object> entries : props.entrySet()) {
String key = (String) entries.getKey(); String key = (String) entries.getKey();
String value = (String) entries.getValue(); String value = (String) entries.getValue();
@@ -82,6 +83,7 @@ public final class TerminalColorScheme {
colorIndex = TextStyle.COLOR_INDEX_BACKGROUND; colorIndex = TextStyle.COLOR_INDEX_BACKGROUND;
} else if (key.equals("cursor")) { } else if (key.equals("cursor")) {
colorIndex = TextStyle.COLOR_INDEX_CURSOR; colorIndex = TextStyle.COLOR_INDEX_CURSOR;
cursorPropExists = true;
} else if (key.startsWith("color")) { } else if (key.startsWith("color")) {
try { try {
colorIndex = Integer.parseInt(key.substring(5)); colorIndex = Integer.parseInt(key.substring(5));
@@ -98,6 +100,27 @@ public final class TerminalColorScheme {
mDefaultColors[colorIndex] = colorValue; mDefaultColors[colorIndex] = colorValue;
} }
if (!cursorPropExists)
setCursorColorForBackground();
}
/**
* If the "cursor" color is not set by user, we need to decide on the appropriate color that will
* be visible on the current terminal background. White will not be visible on light backgrounds
* and black won't be visible on dark backgrounds. So we find the perceived brightness of the
* background color and if its below the threshold (too dark), we use white cursor and if its
* above (too bright), we use black cursor.
*/
public void setCursorColorForBackground() {
int backgroundColor = mDefaultColors[TextStyle.COLOR_INDEX_BACKGROUND];
int brightness = TerminalColors.getPerceivedBrightnessOfColor(backgroundColor);
if (brightness > 0) {
if (brightness < 130)
mDefaultColors[TextStyle.COLOR_INDEX_CURSOR] = 0xffffffff;
else
mDefaultColors[TextStyle.COLOR_INDEX_CURSOR] = 0xff000000;
}
} }
} }

View File

@@ -1,5 +1,7 @@
package com.termux.terminal; package com.termux.terminal;
import android.graphics.Color;
/** Current terminal colors (if different from default). */ /** Current terminal colors (if different from default). */
public final class TerminalColors { public final class TerminalColors {
@@ -73,4 +75,22 @@ public final class TerminalColors {
if (c != 0) mCurrentColors[intoIndex] = c; if (c != 0) mCurrentColors[intoIndex] = c;
} }
/**
* Get the perceived brightness of the color based on its RGB components.
*
* https://www.nbdtech.com/Blog/archive/2008/04/27/Calculating-the-Perceived-Brightness-of-a-Color.aspx
* http://alienryderflex.com/hsp.html
*
* @param color The color code int.
* @return Returns value between 0-255.
*/
public static int getPerceivedBrightnessOfColor(int color) {
return (int)
Math.floor(Math.sqrt(
Math.pow(Color.red(color), 2) * 0.241 +
Math.pow(Color.green(color), 2) * 0.691 +
Math.pow(Color.blue(color), 2) * 0.068
));
}
} }

View File

@@ -126,6 +126,10 @@ public final class TerminalEmulator {
private String mTitle; private String mTitle;
private final Stack<String> mTitleStack = new Stack<>(); 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). */ /** The cursor position. Between (0,0) and (mRows-1, mColumns-1). */
private int mCursorRow, mCursorCol; private int mCursorRow, mCursorCol;
@@ -796,7 +800,6 @@ public final class TerminalEmulator {
int columnsToDelete = Math.min(getArg0(1), columnsAfterCursor); int columnsToDelete = Math.min(getArg0(1), columnsAfterCursor);
int columnsToMove = columnsAfterCursor - columnsToDelete; int columnsToMove = columnsAfterCursor - columnsToDelete;
mScreen.blockCopy(mCursorCol + columnsToDelete, 0, columnsToMove, mRows, mCursorCol, 0); mScreen.blockCopy(mCursorCol + columnsToDelete, 0, columnsToMove, mRows, mCursorCol, 0);
blockClear(mCursorRow + columnsToMove, 0, columnsToDelete, mRows);
} else { } else {
unknownSequence(b); unknownSequence(b);
} }
@@ -825,7 +828,7 @@ public final class TerminalEmulator {
if (internalBit != -1) { if (internalBit != -1) {
value = isDecsetInternalBitSet(internalBit) ? 1 : 2; // 1=set, 2=reset. value = isDecsetInternalBitSet(internalBit) ? 1 : 2; // 1=set, 2=reset.
} else { } else {
mClient.logError(LOG_TAG, "Got DECRQM for unrecognized private DEC mode=" + mode); Logger.logError(mClient, LOG_TAG, "Got DECRQM for unrecognized private DEC mode=" + mode);
value = 0; // 0=not recognized, 3=permanently set, 4=permanently reset value = 0; // 0=not recognized, 3=permanently set, 4=permanently reset
} }
} }
@@ -936,10 +939,17 @@ public final class TerminalEmulator {
for (String part : dcs.substring(2).split(";")) { for (String part : dcs.substring(2).split(";")) {
if (part.length() % 2 == 0) { if (part.length() % 2 == 0) {
StringBuilder transBuffer = new StringBuilder(); StringBuilder transBuffer = new StringBuilder();
char c;
for (int i = 0; i < part.length(); i += 2) { for (int i = 0; i < part.length(); i += 2) {
char c = (char) Long.decode("0x" + part.charAt(i) + "" + part.charAt(i + 1)).longValue(); try {
c = (char) Long.decode("0x" + part.charAt(i) + "" + part.charAt(i + 1)).longValue();
} catch (NumberFormatException e) {
Logger.logStackTraceWithMessage(mClient, LOG_TAG, "Invalid device termcap/terminfo encoded name \"" + part + "\"", e);
continue;
}
transBuffer.append(c); transBuffer.append(c);
} }
String trans = transBuffer.toString(); String trans = transBuffer.toString();
String responseValue; String responseValue;
switch (trans) { switch (trans) {
@@ -962,7 +972,7 @@ public final class TerminalEmulator {
case "&8": // Undo key - ignore. case "&8": // Undo key - ignore.
break; break;
default: default:
mClient.logWarn(LOG_TAG, "Unhandled termcap/terminfo name: '" + trans + "'"); Logger.logWarn(mClient, LOG_TAG, "Unhandled termcap/terminfo name: '" + trans + "'");
} }
// Respond with invalid request: // Respond with invalid request:
mSession.write("\033P0+r" + part + "\033\\"); mSession.write("\033P0+r" + part + "\033\\");
@@ -974,12 +984,12 @@ public final class TerminalEmulator {
mSession.write("\033P1+r" + part + "=" + hexEncoded + "\033\\"); mSession.write("\033P1+r" + part + "=" + hexEncoded + "\033\\");
} }
} else { } else {
mClient.logError(LOG_TAG, "Invalid device termcap/terminfo name of odd length: " + part); Logger.logError(mClient, LOG_TAG, "Invalid device termcap/terminfo name of odd length: " + part);
} }
} }
} else { } else {
if (LOG_ESCAPE_SEQUENCES) if (LOG_ESCAPE_SEQUENCES)
mClient.logError(LOG_TAG, "Unrecognized device control string: " + dcs); Logger.logError(mClient, LOG_TAG, "Unrecognized device control string: " + dcs);
} }
finishSequence(); finishSequence();
} }
@@ -1069,7 +1079,7 @@ public final class TerminalEmulator {
int externalBit = mArgs[i]; int externalBit = mArgs[i];
int internalBit = mapDecSetBitToInternalBit(externalBit); int internalBit = mapDecSetBitToInternalBit(externalBit);
if (internalBit == -1) { if (internalBit == -1) {
mClient.logWarn(LOG_TAG, "Ignoring request to save/recall decset bit=" + externalBit); Logger.logWarn(mClient, LOG_TAG, "Ignoring request to save/recall decset bit=" + externalBit);
} else { } else {
if (b == 's') { if (b == 's') {
mSavedDecSetFlags |= internalBit; mSavedDecSetFlags |= internalBit;
@@ -1259,7 +1269,7 @@ public final class TerminalEmulator {
// (1) enables this feature for keys except for those with well-known behavior, e.g., Tab, Backarrow and // (1) enables this feature for keys except for those with well-known behavior, e.g., Tab, Backarrow and
// some special control character cases, e.g., Control-Space to make a NUL. // some special control character cases, e.g., Control-Space to make a NUL.
// (2) enables this feature for keys including the exceptions listed. // (2) enables this feature for keys including the exceptions listed.
mClient.logError(LOG_TAG, "(ignored) CSI > MODIFY RESOURCE: " + getArg0(-1) + " to " + getArg1(-1)); Logger.logError(mClient, LOG_TAG, "(ignored) CSI > MODIFY RESOURCE: " + getArg0(-1) + " to " + getArg1(-1));
break; break;
default: default:
parseArg(b); parseArg(b);
@@ -1380,6 +1390,8 @@ public final class TerminalEmulator {
break; break;
case '[': case '[':
continueSequence(ESC_CSI); continueSequence(ESC_CSI);
mIsCSIStart = true;
mLastCSIArg = null;
break; break;
case '=': // DECKPAM case '=': // DECKPAM
setDecsetinternalBit(DECSET_BIT_APPLICATION_KEYPAD, true); setDecsetinternalBit(DECSET_BIT_APPLICATION_KEYPAD, true);
@@ -1806,7 +1818,7 @@ public final class TerminalEmulator {
int firstArg = mArgs[i + 1]; int firstArg = mArgs[i + 1];
if (firstArg == 2) { if (firstArg == 2) {
if (i + 4 > mArgIndex) { if (i + 4 > mArgIndex) {
mClient.logWarn(LOG_TAG, "Too few CSI" + code + ";2 RGB arguments"); Logger.logWarn(mClient, LOG_TAG, "Too few CSI" + code + ";2 RGB arguments");
} else { } else {
int red = mArgs[i + 2], green = mArgs[i + 3], blue = mArgs[i + 4]; int red = mArgs[i + 2], green = mArgs[i + 3], blue = mArgs[i + 4];
if (red < 0 || green < 0 || blue < 0 || red > 255 || green > 255 || blue > 255) { if (red < 0 || green < 0 || blue < 0 || red > 255 || green > 255 || blue > 255) {
@@ -1831,7 +1843,7 @@ public final class TerminalEmulator {
mBackColor = color; mBackColor = color;
} }
} else { } else {
if (LOG_ESCAPE_SEQUENCES) mClient.logWarn(LOG_TAG, "Invalid color index: " + color); if (LOG_ESCAPE_SEQUENCES) Logger.logWarn(mClient, LOG_TAG, "Invalid color index: " + color);
} }
} else { } else {
finishSequenceAndLogError("Invalid ISO-8613-3 SGR first argument: " + firstArg); finishSequenceAndLogError("Invalid ISO-8613-3 SGR first argument: " + firstArg);
@@ -1848,7 +1860,7 @@ public final class TerminalEmulator {
mBackColor = code - 100 + 8; mBackColor = code - 100 + 8;
} else { } else {
if (LOG_ESCAPE_SEQUENCES) if (LOG_ESCAPE_SEQUENCES)
mClient.logWarn(LOG_TAG, String.format("SGR unknown code %d", code)); Logger.logWarn(mClient, LOG_TAG, String.format("SGR unknown code %d", code));
} }
} }
} }
@@ -1980,9 +1992,9 @@ public final class TerminalEmulator {
int startIndex = textParameter.indexOf(";") + 1; int startIndex = textParameter.indexOf(";") + 1;
try { try {
String clipboardText = new String(Base64.decode(textParameter.substring(startIndex), 0), StandardCharsets.UTF_8); String clipboardText = new String(Base64.decode(textParameter.substring(startIndex), 0), StandardCharsets.UTF_8);
mSession.clipboardText(clipboardText); mSession.onCopyTextToClipboard(clipboardText);
} catch (Exception e) { } catch (Exception e) {
mClient.logError(LOG_TAG, "OSC Manipulate selection, invalid string '" + textParameter + ""); Logger.logError(mClient, LOG_TAG, "OSC Manipulate selection, invalid string '" + textParameter + "");
} }
break; break;
case 104: case 104:
@@ -2087,28 +2099,57 @@ public final class TerminalEmulator {
} }
} }
/** Process the next ASCII character of a parameter. */ /**
private void parseArg(int b) { * Process the next ASCII character of a parameter.
if (b >= '0' && b <= '9') { *
if (mArgIndex < mArgs.length) { * Parameter characters modify the action or interpretation of the sequence. You can use up to
int oldValue = mArgs[mArgIndex]; * 16 parameters per sequence. You must use the ; character to separate parameters.
int thisDigit = b - '0'; * All parameters are unsigned, positive decimal integers, with the most significant
int value; * digit sent first. Any parameter greater than 9999 (decimal) is set to 9999
if (oldValue >= 0) { * (decimal). If you do not specify a value, a 0 value is assumed. A 0 value
value = oldValue * 10 + thisDigit; * or omitted parameter indicates a default value for the sequence. For most
} else { * sequences, the default value is 1.
value = thisDigit; *
* https://vt100.net/docs/vt510-rm/chapter4.html#S4.3.3
* */
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
}
}
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;
} }
mArgs[mArgIndex] = value; continueSequence(mEscapeState);
} else if (b == ';') {
if (mArgIndex < mArgs.length) {
mArgIndex++;
}
continueSequence(mEscapeState);
} else {
unknownSequence(b);
} }
continueSequence(mEscapeState); mLastCSIArg = b;
} else if (b == ';') {
if (mArgIndex < mArgs.length) {
mArgIndex++;
}
continueSequence(mEscapeState);
} else {
unknownSequence(b);
} }
} }
@@ -2178,7 +2219,7 @@ public final class TerminalEmulator {
} }
private void finishSequenceAndLogError(String error) { private void finishSequenceAndLogError(String error) {
if (LOG_ESCAPE_SEQUENCES) mClient.logWarn(LOG_TAG, error); if (LOG_ESCAPE_SEQUENCES) Logger.logWarn(mClient, LOG_TAG, error);
finishSequence(); finishSequence();
} }
@@ -2326,7 +2367,14 @@ public final class TerminalEmulator {
} }
int offsetDueToCombiningChar = ((displayWidth <= 0 && mCursorCol > 0 && !mAboutToAutoWrap) ? 1 : 0); int offsetDueToCombiningChar = ((displayWidth <= 0 && mCursorCol > 0 && !mAboutToAutoWrap) ? 1 : 0);
mScreen.setChar(mCursorCol - offsetDueToCombiningChar, mCursorRow, codePoint, getStyle()); int column = mCursorCol - offsetDueToCombiningChar;
// Fix TerminalRow.setChar() ArrayIndexOutOfBoundsException index=-1 exception reported
// The offsetDueToCombiningChar would never be 1 if mCursorCol was 0 to get column/index=-1,
// so was mCursorCol changed after the offsetDueToCombiningChar conditional by another thread?
// TODO: Check if there are thread synchronization issues with mCursorCol and mCursorRow, possibly causing others bugs too.
if (column < 0) column = 0;
mScreen.setChar(column, mCursorRow, codePoint, getStyle());
if (autoWrap && displayWidth > 0) if (autoWrap && displayWidth > 0)
mAboutToAutoWrap = (mCursorCol == mRightMargin - displayWidth); mAboutToAutoWrap = (mCursorCol == mRightMargin - displayWidth);

View File

@@ -18,8 +18,11 @@ public abstract class TerminalOutput {
/** Notify the terminal client that the terminal title has changed. */ /** Notify the terminal client that the terminal title has changed. */
public abstract void titleChanged(String oldTitle, String newTitle); public abstract void titleChanged(String oldTitle, String newTitle);
/** Notify the terminal client that the terminal title has changed. */ /** Notify the terminal client that text should be copied to clipboard. */
public abstract void clipboardText(String text); public abstract void onCopyTextToClipboard(String text);
/** Notify the terminal client that text should be pasted from clipboard. */
public abstract void onPasteTextFromClipboard();
/** Notify the terminal client that a bell character (ASCII 7, bell, BEL, \a, ^G)) has been received. */ /** Notify the terminal client that a bell character (ASCII 7, bell, BEL, \a, ^G)) has been received. */
public abstract void onBell(); public abstract void onBell();

View File

@@ -11,11 +11,37 @@ public final class TerminalRow {
private static final float SPARE_CAPACITY_FACTOR = 1.5f; private static final float SPARE_CAPACITY_FACTOR = 1.5f;
/**
* Max combining characters that can exist in a column, that are separate from the base character
* itself. Any additional combining characters will be ignored and not added to the column.
*
* There does not seem to be limit in unicode standard for max number of combination characters
* that can be combined but such characters are primarily under 10.
*
* "Section 3.6 Combination" of unicode standard contains combining characters info.
* - https://www.unicode.org/versions/Unicode15.0.0/ch03.pdf
* - https://en.wikipedia.org/wiki/Combining_character#Unicode_ranges
* - https://stackoverflow.com/questions/71237212/what-is-the-maximum-number-of-unicode-combined-characters-that-may-be-needed-to
*
* UAX15-D3 Stream-Safe Text Format limits to max 30 combining characters.
* > The value of 30 is chosen to be significantly beyond what is required for any linguistic or technical usage.
* > While it would have been feasible to chose a smaller number, this value provides a very wide margin,
* > yet is well within the buffer size limits of practical implementations.
* - https://unicode.org/reports/tr15/#Stream_Safe_Text_Format
* - https://stackoverflow.com/a/11983435/14686958
*
* We choose the value 15 because it should be enough for terminal based applications and keep
* the memory usage low for a terminal row, won't affect performance or cause terminal to
* lag or hang, and will keep malicious applications from causing harm. The value can be
* increased if ever needed for legitimate applications.
*/
private static final int MAX_COMBINING_CHARACTERS_PER_COLUMN = 15;
/** The number of columns in this terminal row. */ /** The number of columns in this terminal row. */
private final int mColumns; private final int mColumns;
/** The text filling this terminal row. */ /** The text filling this terminal row. */
public char[] mText; public char[] mText;
/** The number of java char:s used in {@link #mText}. */ /** The number of java chars used in {@link #mText}. */
private short mSpaceUsed; private short mSpaceUsed;
/** If this row has been line wrapped due to text output at the end of line. */ /** If this row has been line wrapped due to text output at the end of line. */
boolean mLineWrap; boolean mLineWrap;
@@ -124,6 +150,9 @@ public final class TerminalRow {
// https://github.com/steven676/Android-Terminal-Emulator/commit/9a47042620bec87617f0b4f5d50568535668fe26 // https://github.com/steven676/Android-Terminal-Emulator/commit/9a47042620bec87617f0b4f5d50568535668fe26
public void setChar(int columnToSet, int codePoint, long style) { public void setChar(int columnToSet, int codePoint, long style) {
if (columnToSet < 0 || columnToSet >= mStyle.length)
throw new IllegalArgumentException("TerminalRow.setChar(): columnToSet=" + columnToSet + ", codePoint=" + codePoint + ", style=" + style);
mStyle[columnToSet] = style; mStyle[columnToSet] = style;
final int newCodePointDisplayWidth = WcWidth.width(codePoint); final int newCodePointDisplayWidth = WcWidth.width(codePoint);
@@ -160,18 +189,25 @@ public final class TerminalRow {
// Get the number of elements in the mText array this column uses now // Get the number of elements in the mText array this column uses now
int oldCharactersUsedForColumn; int oldCharactersUsedForColumn;
if (columnToSet + oldCodePointDisplayWidth < mColumns) { if (columnToSet + oldCodePointDisplayWidth < mColumns) {
oldCharactersUsedForColumn = findStartOfColumn(columnToSet + oldCodePointDisplayWidth) - oldStartOfColumnIndex; int oldEndOfColumnIndex = findStartOfColumn(columnToSet + oldCodePointDisplayWidth);
oldCharactersUsedForColumn = oldEndOfColumnIndex - oldStartOfColumnIndex;
} else { } else {
// Last character. // Last character.
oldCharactersUsedForColumn = mSpaceUsed - oldStartOfColumnIndex; oldCharactersUsedForColumn = mSpaceUsed - oldStartOfColumnIndex;
} }
// If MAX_COMBINING_CHARACTERS_PER_COLUMN already exist in column, then ignore adding additional combining characters.
if (newIsCombining) {
int combiningCharsCount = WcWidth.zeroWidthCharsCount(mText, oldStartOfColumnIndex, oldStartOfColumnIndex + oldCharactersUsedForColumn);
if (combiningCharsCount >= MAX_COMBINING_CHARACTERS_PER_COLUMN)
return;
}
// Find how many chars this column will need // Find how many chars this column will need
int newCharactersUsedForColumn = Character.charCount(codePoint); int newCharactersUsedForColumn = Character.charCount(codePoint);
if (newIsCombining) { if (newIsCombining) {
// Combining characters are added to the contents of the column instead of overwriting them, so that they // Combining characters are added to the contents of the column instead of overwriting them, so that they
// modify the existing contents. // modify the existing contents.
// FIXME: Put a limit of combining characters.
// FIXME: Unassigned characters also get width=0. // FIXME: Unassigned characters also get width=0.
newCharactersUsedForColumn += oldCharactersUsedForColumn; newCharactersUsedForColumn += oldCharactersUsedForColumn;
} }
@@ -186,7 +222,7 @@ public final class TerminalRow {
if (mSpaceUsed + javaCharDifference > text.length) { if (mSpaceUsed + javaCharDifference > text.length) {
// We need to grow the array // We need to grow the array
char[] newText = new char[text.length + mColumns]; char[] newText = new char[text.length + mColumns];
System.arraycopy(text, 0, newText, 0, oldStartOfColumnIndex + oldCharactersUsedForColumn); System.arraycopy(text, 0, newText, 0, oldNextColumnIndex);
System.arraycopy(text, oldNextColumnIndex, newText, newNextColumnIndex, oldCharactersAfterColumn); System.arraycopy(text, oldNextColumnIndex, newText, newNextColumnIndex, oldCharactersAfterColumn);
mText = text = newText; mText = text = newText;
} else { } else {

View File

@@ -236,7 +236,7 @@ public final class TerminalSession extends TerminalOutput {
try { try {
Os.kill(mShellPid, OsConstants.SIGKILL); Os.kill(mShellPid, OsConstants.SIGKILL);
} catch (ErrnoException e) { } catch (ErrnoException e) {
mClient.logWarn(LOG_TAG, "Failed sending SIGKILL: " + e.getMessage()); Logger.logWarn(mClient, LOG_TAG, "Failed sending SIGKILL: " + e.getMessage());
} }
} }
} }
@@ -269,8 +269,13 @@ public final class TerminalSession extends TerminalOutput {
} }
@Override @Override
public void clipboardText(String text) { public void onCopyTextToClipboard(String text) {
mClient.onClipboardText(this, text); mClient.onCopyTextToClipboard(this, text);
}
@Override
public void onPasteTextFromClipboard() {
mClient.onPasteTextFromClipboard(this);
} }
@Override @Override
@@ -303,7 +308,7 @@ public final class TerminalSession extends TerminalOutput {
return outputPath; return outputPath;
} }
} catch (IOException | SecurityException e) { } catch (IOException | SecurityException e) {
mClient.logStackTraceWithMessage(LOG_TAG, "Error getting current directory", e); Logger.logStackTraceWithMessage(mClient, LOG_TAG, "Error getting current directory", e);
} }
return null; return null;
} }
@@ -321,7 +326,7 @@ public final class TerminalSession extends TerminalOutput {
descriptorField.setAccessible(true); descriptorField.setAccessible(true);
descriptorField.set(result, fileDescriptor); descriptorField.set(result, fileDescriptor);
} catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) { } catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) {
client.logStackTraceWithMessage(LOG_TAG, "Error accessing FileDescriptor#descriptor private field", e); Logger.logStackTraceWithMessage(client, LOG_TAG, "Error accessing FileDescriptor#descriptor private field", e);
System.exit(1); System.exit(1);
} }
return result; return result;

View File

@@ -13,7 +13,9 @@ public interface TerminalSessionClient {
void onSessionFinished(TerminalSession finishedSession); void onSessionFinished(TerminalSession finishedSession);
void onClipboardText(TerminalSession session, String text); void onCopyTextToClipboard(TerminalSession session, String text);
void onPasteTextFromClipboard(TerminalSession session);
void onBell(TerminalSession session); void onBell(TerminalSession session);

View File

@@ -1,7 +1,7 @@
package com.termux.terminal; package com.termux.terminal;
/** /**
* Implementation of wcwidth(3) for Unicode 9. * Implementation of wcwidth(3) for Unicode 15.
* *
* Implementation from https://github.com/jquast/wcwidth but we return 0 for unprintable characters. * Implementation from https://github.com/jquast/wcwidth but we return 0 for unprintable characters.
* *
@@ -9,12 +9,13 @@ package com.termux.terminal;
* Must be kept in sync with the following: * Must be kept in sync with the following:
* https://github.com/termux/wcwidth * https://github.com/termux/wcwidth
* https://github.com/termux/libandroid-support * https://github.com/termux/libandroid-support
* https://github.com/termux/termux-packages/tree/master/libandroid-support * https://github.com/termux/termux-packages/tree/master/packages/libandroid-support
*/ */
public final class WcWidth { public final class WcWidth {
// From https://github.com/jquast/wcwidth/blob/master/wcwidth/table_zero.py // From https://github.com/jquast/wcwidth/blob/master/wcwidth/table_zero.py
// at commit b29897e5a1b403a0e36f7fc991614981cbc42475 (2020-07-14): // from https://github.com/jquast/wcwidth/pull/64
// at commit 1b9b6585b0080ea5cb88dc9815796505724793fe (2022-12-16):
private static final int[][] ZERO_WIDTH = { private static final int[][] ZERO_WIDTH = {
{0x00300, 0x0036f}, // Combining Grave Accent ..Combining Latin Small Le {0x00300, 0x0036f}, // Combining Grave Accent ..Combining Latin Small Le
{0x00483, 0x00489}, // Combining Cyrillic Titlo..Combining Cyrillic Milli {0x00483, 0x00489}, // Combining Cyrillic Titlo..Combining Cyrillic Milli
@@ -40,7 +41,8 @@ public final class WcWidth {
{0x00825, 0x00827}, // Samaritan Vowel Sign Sho..Samaritan Vowel Sign U {0x00825, 0x00827}, // Samaritan Vowel Sign Sho..Samaritan Vowel Sign U
{0x00829, 0x0082d}, // Samaritan Vowel Sign Lon..Samaritan Mark Nequdaa {0x00829, 0x0082d}, // Samaritan Vowel Sign Lon..Samaritan Mark Nequdaa
{0x00859, 0x0085b}, // Mandaic Affrication Mark..Mandaic Gemination Mark {0x00859, 0x0085b}, // Mandaic Affrication Mark..Mandaic Gemination Mark
{0x008d3, 0x008e1}, // Arabic Small Low Waw ..Arabic Small High Sign S {0x00898, 0x0089f}, // Arabic Small High Word A..Arabic Half Madda Over M
{0x008ca, 0x008e1}, // Arabic Small High Farsi ..Arabic Small High Sign S
{0x008e3, 0x00902}, // Arabic Turned Damma Belo..Devanagari Sign Anusvara {0x008e3, 0x00902}, // Arabic Turned Damma Belo..Devanagari Sign Anusvara
{0x0093a, 0x0093a}, // Devanagari Vowel Sign Oe..Devanagari Vowel Sign Oe {0x0093a, 0x0093a}, // Devanagari Vowel Sign Oe..Devanagari Vowel Sign Oe
{0x0093c, 0x0093c}, // Devanagari Sign Nukta ..Devanagari Sign Nukta {0x0093c, 0x0093c}, // Devanagari Sign Nukta ..Devanagari Sign Nukta
@@ -74,13 +76,14 @@ public final class WcWidth {
{0x00b3f, 0x00b3f}, // Oriya Vowel Sign I ..Oriya Vowel Sign I {0x00b3f, 0x00b3f}, // Oriya Vowel Sign I ..Oriya Vowel Sign I
{0x00b41, 0x00b44}, // Oriya Vowel Sign U ..Oriya Vowel Sign Vocalic {0x00b41, 0x00b44}, // Oriya Vowel Sign U ..Oriya Vowel Sign Vocalic
{0x00b4d, 0x00b4d}, // Oriya Sign Virama ..Oriya Sign Virama {0x00b4d, 0x00b4d}, // Oriya Sign Virama ..Oriya Sign Virama
{0x00b55, 0x00b56}, // (nil) ..Oriya Ai Length Mark {0x00b55, 0x00b56}, // Oriya Sign Overline ..Oriya Ai Length Mark
{0x00b62, 0x00b63}, // Oriya Vowel Sign Vocalic..Oriya Vowel Sign Vocalic {0x00b62, 0x00b63}, // Oriya Vowel Sign Vocalic..Oriya Vowel Sign Vocalic
{0x00b82, 0x00b82}, // Tamil Sign Anusvara ..Tamil Sign Anusvara {0x00b82, 0x00b82}, // Tamil Sign Anusvara ..Tamil Sign Anusvara
{0x00bc0, 0x00bc0}, // Tamil Vowel Sign Ii ..Tamil Vowel Sign Ii {0x00bc0, 0x00bc0}, // Tamil Vowel Sign Ii ..Tamil Vowel Sign Ii
{0x00bcd, 0x00bcd}, // Tamil Sign Virama ..Tamil Sign Virama {0x00bcd, 0x00bcd}, // Tamil Sign Virama ..Tamil Sign Virama
{0x00c00, 0x00c00}, // Telugu Sign Combining Ca..Telugu Sign Combining Ca {0x00c00, 0x00c00}, // Telugu Sign Combining Ca..Telugu Sign Combining Ca
{0x00c04, 0x00c04}, // Telugu Sign Combining An..Telugu Sign Combining An {0x00c04, 0x00c04}, // Telugu Sign Combining An..Telugu Sign Combining An
{0x00c3c, 0x00c3c}, // Telugu Sign Nukta ..Telugu Sign Nukta
{0x00c3e, 0x00c40}, // Telugu Vowel Sign Aa ..Telugu Vowel Sign Ii {0x00c3e, 0x00c40}, // Telugu Vowel Sign Aa ..Telugu Vowel Sign Ii
{0x00c46, 0x00c48}, // Telugu Vowel Sign E ..Telugu Vowel Sign Ai {0x00c46, 0x00c48}, // Telugu Vowel Sign E ..Telugu Vowel Sign Ai
{0x00c4a, 0x00c4d}, // Telugu Vowel Sign O ..Telugu Sign Virama {0x00c4a, 0x00c4d}, // Telugu Vowel Sign O ..Telugu Sign Virama
@@ -97,7 +100,7 @@ public final class WcWidth {
{0x00d41, 0x00d44}, // Malayalam Vowel Sign U ..Malayalam Vowel Sign Voc {0x00d41, 0x00d44}, // Malayalam Vowel Sign U ..Malayalam Vowel Sign Voc
{0x00d4d, 0x00d4d}, // Malayalam Sign Virama ..Malayalam Sign Virama {0x00d4d, 0x00d4d}, // Malayalam Sign Virama ..Malayalam Sign Virama
{0x00d62, 0x00d63}, // Malayalam Vowel Sign Voc..Malayalam Vowel Sign Voc {0x00d62, 0x00d63}, // Malayalam Vowel Sign Voc..Malayalam Vowel Sign Voc
{0x00d81, 0x00d81}, // (nil) ..(nil) {0x00d81, 0x00d81}, // Sinhala Sign Candrabindu..Sinhala Sign Candrabindu
{0x00dca, 0x00dca}, // Sinhala Sign Al-lakuna ..Sinhala Sign Al-lakuna {0x00dca, 0x00dca}, // Sinhala Sign Al-lakuna ..Sinhala Sign Al-lakuna
{0x00dd2, 0x00dd4}, // Sinhala Vowel Sign Ketti..Sinhala Vowel Sign Ketti {0x00dd2, 0x00dd4}, // Sinhala Vowel Sign Ketti..Sinhala Vowel Sign Ketti
{0x00dd6, 0x00dd6}, // Sinhala Vowel Sign Diga ..Sinhala Vowel Sign Diga {0x00dd6, 0x00dd6}, // Sinhala Vowel Sign Diga ..Sinhala Vowel Sign Diga
@@ -106,7 +109,7 @@ public final class WcWidth {
{0x00e47, 0x00e4e}, // Thai Character Maitaikhu..Thai Character Yamakkan {0x00e47, 0x00e4e}, // Thai Character Maitaikhu..Thai Character Yamakkan
{0x00eb1, 0x00eb1}, // Lao Vowel Sign Mai Kan ..Lao Vowel Sign Mai Kan {0x00eb1, 0x00eb1}, // Lao Vowel Sign Mai Kan ..Lao Vowel Sign Mai Kan
{0x00eb4, 0x00ebc}, // Lao Vowel Sign I ..Lao Semivowel Sign Lo {0x00eb4, 0x00ebc}, // Lao Vowel Sign I ..Lao Semivowel Sign Lo
{0x00ec8, 0x00ecd}, // Lao Tone Mai Ek ..Lao Niggahita {0x00ec8, 0x00ece}, // Lao Tone Mai Ek ..(nil)
{0x00f18, 0x00f19}, // Tibetan Astrological Sig..Tibetan Astrological Sig {0x00f18, 0x00f19}, // Tibetan Astrological Sig..Tibetan Astrological Sig
{0x00f35, 0x00f35}, // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung {0x00f35, 0x00f35}, // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung
{0x00f37, 0x00f37}, // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung {0x00f37, 0x00f37}, // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung
@@ -130,7 +133,7 @@ public final class WcWidth {
{0x0109d, 0x0109d}, // Myanmar Vowel Sign Aiton..Myanmar Vowel Sign Aiton {0x0109d, 0x0109d}, // Myanmar Vowel Sign Aiton..Myanmar Vowel Sign Aiton
{0x0135d, 0x0135f}, // Ethiopic Combining Gemin..Ethiopic Combining Gemin {0x0135d, 0x0135f}, // Ethiopic Combining Gemin..Ethiopic Combining Gemin
{0x01712, 0x01714}, // Tagalog Vowel Sign I ..Tagalog Sign Virama {0x01712, 0x01714}, // Tagalog Vowel Sign I ..Tagalog Sign Virama
{0x01732, 0x01734}, // Hanunoo Vowel Sign I ..Hanunoo Sign Pamudpod {0x01732, 0x01733}, // Hanunoo Vowel Sign I ..Hanunoo Vowel Sign U
{0x01752, 0x01753}, // Buhid Vowel Sign I ..Buhid Vowel Sign U {0x01752, 0x01753}, // Buhid Vowel Sign I ..Buhid Vowel Sign U
{0x01772, 0x01773}, // Tagbanwa Vowel Sign I ..Tagbanwa Vowel Sign U {0x01772, 0x01773}, // Tagbanwa Vowel Sign I ..Tagbanwa Vowel Sign U
{0x017b4, 0x017b5}, // Khmer Vowel Inherent Aq ..Khmer Vowel Inherent Aa {0x017b4, 0x017b5}, // Khmer Vowel Inherent Aq ..Khmer Vowel Inherent Aa
@@ -139,6 +142,7 @@ public final class WcWidth {
{0x017c9, 0x017d3}, // Khmer Sign Muusikatoan ..Khmer Sign Bathamasat {0x017c9, 0x017d3}, // Khmer Sign Muusikatoan ..Khmer Sign Bathamasat
{0x017dd, 0x017dd}, // Khmer Sign Atthacan ..Khmer Sign Atthacan {0x017dd, 0x017dd}, // Khmer Sign Atthacan ..Khmer Sign Atthacan
{0x0180b, 0x0180d}, // Mongolian Free Variation..Mongolian Free Variation {0x0180b, 0x0180d}, // Mongolian Free Variation..Mongolian Free Variation
{0x0180f, 0x0180f}, // Mongolian Free Variation..Mongolian Free Variation
{0x01885, 0x01886}, // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal {0x01885, 0x01886}, // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal
{0x018a9, 0x018a9}, // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal {0x018a9, 0x018a9}, // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal
{0x01920, 0x01922}, // Limbu Vowel Sign A ..Limbu Vowel Sign U {0x01920, 0x01922}, // Limbu Vowel Sign A ..Limbu Vowel Sign U
@@ -154,7 +158,7 @@ public final class WcWidth {
{0x01a65, 0x01a6c}, // Tai Tham Vowel Sign I ..Tai Tham Vowel Sign Oa B {0x01a65, 0x01a6c}, // Tai Tham Vowel Sign I ..Tai Tham Vowel Sign Oa B
{0x01a73, 0x01a7c}, // Tai Tham Vowel Sign Oa A..Tai Tham Sign Khuen-lue {0x01a73, 0x01a7c}, // Tai Tham Vowel Sign Oa A..Tai Tham Sign Khuen-lue
{0x01a7f, 0x01a7f}, // Tai Tham Combining Crypt..Tai Tham Combining Crypt {0x01a7f, 0x01a7f}, // Tai Tham Combining Crypt..Tai Tham Combining Crypt
{0x01ab0, 0x01ac0}, // Combining Doubled Circum..(nil) {0x01ab0, 0x01ace}, // Combining Doubled Circum..Combining Latin Small Le
{0x01b00, 0x01b03}, // Balinese Sign Ulu Ricem ..Balinese Sign Surang {0x01b00, 0x01b03}, // Balinese Sign Ulu Ricem ..Balinese Sign Surang
{0x01b34, 0x01b34}, // Balinese Sign Rerekan ..Balinese Sign Rerekan {0x01b34, 0x01b34}, // Balinese Sign Rerekan ..Balinese Sign Rerekan
{0x01b36, 0x01b3a}, // Balinese Vowel Sign Ulu ..Balinese Vowel Sign Ra R {0x01b36, 0x01b3a}, // Balinese Vowel Sign Ulu ..Balinese Vowel Sign Ra R
@@ -177,8 +181,7 @@ public final class WcWidth {
{0x01ced, 0x01ced}, // Vedic Sign Tiryak ..Vedic Sign Tiryak {0x01ced, 0x01ced}, // Vedic Sign Tiryak ..Vedic Sign Tiryak
{0x01cf4, 0x01cf4}, // Vedic Tone Candra Above ..Vedic Tone Candra Above {0x01cf4, 0x01cf4}, // Vedic Tone Candra Above ..Vedic Tone Candra Above
{0x01cf8, 0x01cf9}, // Vedic Tone Ring Above ..Vedic Tone Double Ring A {0x01cf8, 0x01cf9}, // Vedic Tone Ring Above ..Vedic Tone Double Ring A
{0x01dc0, 0x01df9}, // Combining Dotted Grave A..Combining Wide Inverted {0x01dc0, 0x01dff}, // Combining Dotted Grave A..Combining Right Arrowhea
{0x01dfb, 0x01dff}, // Combining Deletion Mark ..Combining Right Arrowhea
{0x020d0, 0x020f0}, // Combining Left Harpoon A..Combining Asterisk Above {0x020d0, 0x020f0}, // Combining Left Harpoon A..Combining Asterisk Above
{0x02cef, 0x02cf1}, // Coptic Combining Ni Abov..Coptic Combining Spiritu {0x02cef, 0x02cf1}, // Coptic Combining Ni Abov..Coptic Combining Spiritu
{0x02d7f, 0x02d7f}, // Tifinagh Consonant Joine..Tifinagh Consonant Joine {0x02d7f, 0x02d7f}, // Tifinagh Consonant Joine..Tifinagh Consonant Joine
@@ -193,7 +196,7 @@ public final class WcWidth {
{0x0a806, 0x0a806}, // Syloti Nagri Sign Hasant..Syloti Nagri Sign Hasant {0x0a806, 0x0a806}, // Syloti Nagri Sign Hasant..Syloti Nagri Sign Hasant
{0x0a80b, 0x0a80b}, // Syloti Nagri Sign Anusva..Syloti Nagri Sign Anusva {0x0a80b, 0x0a80b}, // Syloti Nagri Sign Anusva..Syloti Nagri Sign Anusva
{0x0a825, 0x0a826}, // Syloti Nagri Vowel Sign ..Syloti Nagri Vowel Sign {0x0a825, 0x0a826}, // Syloti Nagri Vowel Sign ..Syloti Nagri Vowel Sign
{0x0a82c, 0x0a82c}, // (nil) ..(nil) {0x0a82c, 0x0a82c}, // Syloti Nagri Sign Altern..Syloti Nagri Sign Altern
{0x0a8c4, 0x0a8c5}, // Saurashtra Sign Virama ..Saurashtra Sign Candrabi {0x0a8c4, 0x0a8c5}, // Saurashtra Sign Virama ..Saurashtra Sign Candrabi
{0x0a8e0, 0x0a8f1}, // Combining Devanagari Dig..Combining Devanagari Sig {0x0a8e0, 0x0a8f1}, // Combining Devanagari Dig..Combining Devanagari Sig
{0x0a8ff, 0x0a8ff}, // Devanagari Vowel Sign Ay..Devanagari Vowel Sign Ay {0x0a8ff, 0x0a8ff}, // Devanagari Vowel Sign Ay..Devanagari Vowel Sign Ay
@@ -233,13 +236,18 @@ public final class WcWidth {
{0x10a3f, 0x10a3f}, // Kharoshthi Virama ..Kharoshthi Virama {0x10a3f, 0x10a3f}, // Kharoshthi Virama ..Kharoshthi Virama
{0x10ae5, 0x10ae6}, // Manichaean Abbreviation ..Manichaean Abbreviation {0x10ae5, 0x10ae6}, // Manichaean Abbreviation ..Manichaean Abbreviation
{0x10d24, 0x10d27}, // Hanifi Rohingya Sign Har..Hanifi Rohingya Sign Tas {0x10d24, 0x10d27}, // Hanifi Rohingya Sign Har..Hanifi Rohingya Sign Tas
{0x10eab, 0x10eac}, // (nil) ..(nil) {0x10eab, 0x10eac}, // Yezidi Combining Hamza M..Yezidi Combining Madda M
{0x10efd, 0x10eff}, // (nil) ..(nil)
{0x10f46, 0x10f50}, // Sogdian Combining Dot Be..Sogdian Combining Stroke {0x10f46, 0x10f50}, // Sogdian Combining Dot Be..Sogdian Combining Stroke
{0x10f82, 0x10f85}, // Old Uyghur Combining Dot..Old Uyghur Combining Two
{0x11001, 0x11001}, // Brahmi Sign Anusvara ..Brahmi Sign Anusvara {0x11001, 0x11001}, // Brahmi Sign Anusvara ..Brahmi Sign Anusvara
{0x11038, 0x11046}, // Brahmi Vowel Sign Aa ..Brahmi Virama {0x11038, 0x11046}, // Brahmi Vowel Sign Aa ..Brahmi Virama
{0x11070, 0x11070}, // Brahmi Sign Old Tamil Vi..Brahmi Sign Old Tamil Vi
{0x11073, 0x11074}, // Brahmi Vowel Sign Old Ta..Brahmi Vowel Sign Old Ta
{0x1107f, 0x11081}, // Brahmi Number Joiner ..Kaithi Sign Anusvara {0x1107f, 0x11081}, // Brahmi Number Joiner ..Kaithi Sign Anusvara
{0x110b3, 0x110b6}, // Kaithi Vowel Sign U ..Kaithi Vowel Sign Ai {0x110b3, 0x110b6}, // Kaithi Vowel Sign U ..Kaithi Vowel Sign Ai
{0x110b9, 0x110ba}, // Kaithi Sign Virama ..Kaithi Sign Nukta {0x110b9, 0x110ba}, // Kaithi Sign Virama ..Kaithi Sign Nukta
{0x110c2, 0x110c2}, // Kaithi Vowel Sign Vocali..Kaithi Vowel Sign Vocali
{0x11100, 0x11102}, // Chakma Sign Candrabindu ..Chakma Sign Visarga {0x11100, 0x11102}, // Chakma Sign Candrabindu ..Chakma Sign Visarga
{0x11127, 0x1112b}, // Chakma Vowel Sign A ..Chakma Vowel Sign Uu {0x11127, 0x1112b}, // Chakma Vowel Sign A ..Chakma Vowel Sign Uu
{0x1112d, 0x11134}, // Chakma Vowel Sign Ai ..Chakma Maayyaa {0x1112d, 0x11134}, // Chakma Vowel Sign Ai ..Chakma Maayyaa
@@ -247,11 +255,12 @@ public final class WcWidth {
{0x11180, 0x11181}, // Sharada Sign Candrabindu..Sharada Sign Anusvara {0x11180, 0x11181}, // Sharada Sign Candrabindu..Sharada Sign Anusvara
{0x111b6, 0x111be}, // Sharada Vowel Sign U ..Sharada Vowel Sign O {0x111b6, 0x111be}, // Sharada Vowel Sign U ..Sharada Vowel Sign O
{0x111c9, 0x111cc}, // Sharada Sandhi Mark ..Sharada Extra Short Vowe {0x111c9, 0x111cc}, // Sharada Sandhi Mark ..Sharada Extra Short Vowe
{0x111cf, 0x111cf}, // (nil) ..(nil) {0x111cf, 0x111cf}, // Sharada Sign Inverted Ca..Sharada Sign Inverted Ca
{0x1122f, 0x11231}, // Khojki Vowel Sign U ..Khojki Vowel Sign Ai {0x1122f, 0x11231}, // Khojki Vowel Sign U ..Khojki Vowel Sign Ai
{0x11234, 0x11234}, // Khojki Sign Anusvara ..Khojki Sign Anusvara {0x11234, 0x11234}, // Khojki Sign Anusvara ..Khojki Sign Anusvara
{0x11236, 0x11237}, // Khojki Sign Nukta ..Khojki Sign Shadda {0x11236, 0x11237}, // Khojki Sign Nukta ..Khojki Sign Shadda
{0x1123e, 0x1123e}, // Khojki Sign Sukun ..Khojki Sign Sukun {0x1123e, 0x1123e}, // Khojki Sign Sukun ..Khojki Sign Sukun
{0x11241, 0x11241}, // (nil) ..(nil)
{0x112df, 0x112df}, // Khudawadi Sign Anusvara ..Khudawadi Sign Anusvara {0x112df, 0x112df}, // Khudawadi Sign Anusvara ..Khudawadi Sign Anusvara
{0x112e3, 0x112ea}, // Khudawadi Vowel Sign U ..Khudawadi Sign Virama {0x112e3, 0x112ea}, // Khudawadi Vowel Sign U ..Khudawadi Sign Virama
{0x11300, 0x11301}, // Grantha Sign Combining A..Grantha Sign Candrabindu {0x11300, 0x11301}, // Grantha Sign Combining A..Grantha Sign Candrabindu
@@ -283,9 +292,9 @@ public final class WcWidth {
{0x11727, 0x1172b}, // Ahom Vowel Sign Aw ..Ahom Sign Killer {0x11727, 0x1172b}, // Ahom Vowel Sign Aw ..Ahom Sign Killer
{0x1182f, 0x11837}, // Dogra Vowel Sign U ..Dogra Sign Anusvara {0x1182f, 0x11837}, // Dogra Vowel Sign U ..Dogra Sign Anusvara
{0x11839, 0x1183a}, // Dogra Sign Virama ..Dogra Sign Nukta {0x11839, 0x1183a}, // Dogra Sign Virama ..Dogra Sign Nukta
{0x1193b, 0x1193c}, // (nil) ..(nil) {0x1193b, 0x1193c}, // Dives Akuru Sign Anusvar..Dives Akuru Sign Candrab
{0x1193e, 0x1193e}, // (nil) ..(nil) {0x1193e, 0x1193e}, // Dives Akuru Virama ..Dives Akuru Virama
{0x11943, 0x11943}, // (nil) ..(nil) {0x11943, 0x11943}, // Dives Akuru Sign Nukta ..Dives Akuru Sign Nukta
{0x119d4, 0x119d7}, // Nandinagari Vowel Sign U..Nandinagari Vowel Sign V {0x119d4, 0x119d7}, // Nandinagari Vowel Sign U..Nandinagari Vowel Sign V
{0x119da, 0x119db}, // Nandinagari Vowel Sign E..Nandinagari Vowel Sign A {0x119da, 0x119db}, // Nandinagari Vowel Sign E..Nandinagari Vowel Sign A
{0x119e0, 0x119e0}, // Nandinagari Sign Virama ..Nandinagari Sign Virama {0x119e0, 0x119e0}, // Nandinagari Sign Virama ..Nandinagari Sign Virama
@@ -313,12 +322,20 @@ public final class WcWidth {
{0x11d95, 0x11d95}, // Gunjala Gondi Sign Anusv..Gunjala Gondi Sign Anusv {0x11d95, 0x11d95}, // Gunjala Gondi Sign Anusv..Gunjala Gondi Sign Anusv
{0x11d97, 0x11d97}, // Gunjala Gondi Virama ..Gunjala Gondi Virama {0x11d97, 0x11d97}, // Gunjala Gondi Virama ..Gunjala Gondi Virama
{0x11ef3, 0x11ef4}, // Makasar Vowel Sign I ..Makasar Vowel Sign U {0x11ef3, 0x11ef4}, // Makasar Vowel Sign I ..Makasar Vowel Sign U
{0x11f00, 0x11f01}, // (nil) ..(nil)
{0x11f36, 0x11f3a}, // (nil) ..(nil)
{0x11f40, 0x11f40}, // (nil) ..(nil)
{0x11f42, 0x11f42}, // (nil) ..(nil)
{0x13440, 0x13440}, // (nil) ..(nil)
{0x13447, 0x13455}, // (nil) ..(nil)
{0x16af0, 0x16af4}, // Bassa Vah Combining High..Bassa Vah Combining High {0x16af0, 0x16af4}, // Bassa Vah Combining High..Bassa Vah Combining High
{0x16b30, 0x16b36}, // Pahawh Hmong Mark Cim Tu..Pahawh Hmong Mark Cim Ta {0x16b30, 0x16b36}, // Pahawh Hmong Mark Cim Tu..Pahawh Hmong Mark Cim Ta
{0x16f4f, 0x16f4f}, // Miao Sign Consonant Modi..Miao Sign Consonant Modi {0x16f4f, 0x16f4f}, // Miao Sign Consonant Modi..Miao Sign Consonant Modi
{0x16f8f, 0x16f92}, // Miao Tone Right ..Miao Tone Below {0x16f8f, 0x16f92}, // Miao Tone Right ..Miao Tone Below
{0x16fe4, 0x16fe4}, // (nil) ..(nil) {0x16fe4, 0x16fe4}, // Khitan Small Script Fill..Khitan Small Script Fill
{0x1bc9d, 0x1bc9e}, // Duployan Thick Letter Se..Duployan Double Mark {0x1bc9d, 0x1bc9e}, // Duployan Thick Letter Se..Duployan Double Mark
{0x1cf00, 0x1cf2d}, // Znamenny Combining Mark ..Znamenny Combining Mark
{0x1cf30, 0x1cf46}, // Znamenny Combining Tonal..Znamenny Priznak Modifie
{0x1d167, 0x1d169}, // Musical Symbol Combining..Musical Symbol Combining {0x1d167, 0x1d169}, // Musical Symbol Combining..Musical Symbol Combining
{0x1d17b, 0x1d182}, // Musical Symbol Combining..Musical Symbol Combining {0x1d17b, 0x1d182}, // Musical Symbol Combining..Musical Symbol Combining
{0x1d185, 0x1d18b}, // Musical Symbol Combining..Musical Symbol Combining {0x1d185, 0x1d18b}, // Musical Symbol Combining..Musical Symbol Combining
@@ -335,15 +352,19 @@ public final class WcWidth {
{0x1e01b, 0x1e021}, // Combining Glagolitic Let..Combining Glagolitic Let {0x1e01b, 0x1e021}, // Combining Glagolitic Let..Combining Glagolitic Let
{0x1e023, 0x1e024}, // Combining Glagolitic Let..Combining Glagolitic Let {0x1e023, 0x1e024}, // Combining Glagolitic Let..Combining Glagolitic Let
{0x1e026, 0x1e02a}, // Combining Glagolitic Let..Combining Glagolitic Let {0x1e026, 0x1e02a}, // Combining Glagolitic Let..Combining Glagolitic Let
{0x1e08f, 0x1e08f}, // (nil) ..(nil)
{0x1e130, 0x1e136}, // Nyiakeng Puachue Hmong T..Nyiakeng Puachue Hmong T {0x1e130, 0x1e136}, // Nyiakeng Puachue Hmong T..Nyiakeng Puachue Hmong T
{0x1e2ae, 0x1e2ae}, // Toto Sign Rising Tone ..Toto Sign Rising Tone
{0x1e2ec, 0x1e2ef}, // Wancho Tone Tup ..Wancho Tone Koini {0x1e2ec, 0x1e2ef}, // Wancho Tone Tup ..Wancho Tone Koini
{0x1e4ec, 0x1e4ef}, // (nil) ..(nil)
{0x1e8d0, 0x1e8d6}, // Mende Kikakui Combining ..Mende Kikakui Combining {0x1e8d0, 0x1e8d6}, // Mende Kikakui Combining ..Mende Kikakui Combining
{0x1e944, 0x1e94a}, // Adlam Alif Lengthener ..Adlam Nukta {0x1e944, 0x1e94a}, // Adlam Alif Lengthener ..Adlam Nukta
{0xe0100, 0xe01ef}, // Variation Selector-17 ..Variation Selector-256 {0xe0100, 0xe01ef}, // Variation Selector-17 ..Variation Selector-256
}; };
// https://github.com/jquast/wcwidth/blob/master/wcwidth/table_wide.py // https://github.com/jquast/wcwidth/blob/master/wcwidth/table_wide.py
// at commit b29897e5a1b403a0e36f7fc991614981cbc42475 (2020-07-14): // from https://github.com/jquast/wcwidth/pull/64
// at commit 1b9b6585b0080ea5cb88dc9815796505724793fe (2022-12-16):
private static final int[][] WIDE_EASTASIAN = { private static final int[][] WIDE_EASTASIAN = {
{0x01100, 0x0115f}, // Hangul Choseong Kiyeok ..Hangul Choseong Filler {0x01100, 0x0115f}, // Hangul Choseong Kiyeok ..Hangul Choseong Filler
{0x0231a, 0x0231b}, // Watch ..Hourglass {0x0231a, 0x0231b}, // Watch ..Hourglass
@@ -392,7 +413,7 @@ public final class WcWidth {
{0x03190, 0x031e3}, // Ideographic Annotation L..Cjk Stroke Q {0x03190, 0x031e3}, // Ideographic Annotation L..Cjk Stroke Q
{0x031f0, 0x0321e}, // Katakana Letter Small Ku..Parenthesized Korean Cha {0x031f0, 0x0321e}, // Katakana Letter Small Ku..Parenthesized Korean Cha
{0x03220, 0x03247}, // Parenthesized Ideograph ..Circled Ideograph Koto {0x03220, 0x03247}, // Parenthesized Ideograph ..Circled Ideograph Koto
{0x03250, 0x04dbf}, // Partnership Sign ..(nil) {0x03250, 0x04dbf}, // Partnership Sign ..Cjk Unified Ideograph-4d
{0x04e00, 0x0a48c}, // Cjk Unified Ideograph-4e..Yi Syllable Yyr {0x04e00, 0x0a48c}, // Cjk Unified Ideograph-4e..Yi Syllable Yyr
{0x0a490, 0x0a4c6}, // Yi Radical Qot ..Yi Radical Ke {0x0a490, 0x0a4c6}, // Yi Radical Qot ..Yi Radical Ke
{0x0a960, 0x0a97c}, // Hangul Choseong Tikeut-m..Hangul Choseong Ssangyeo {0x0a960, 0x0a97c}, // Hangul Choseong Tikeut-m..Hangul Choseong Ssangyeo
@@ -404,13 +425,18 @@ public final class WcWidth {
{0x0fe68, 0x0fe6b}, // Small Reverse Solidus ..Small Commercial At {0x0fe68, 0x0fe6b}, // Small Reverse Solidus ..Small Commercial At
{0x0ff01, 0x0ff60}, // Fullwidth Exclamation Ma..Fullwidth Right White Pa {0x0ff01, 0x0ff60}, // Fullwidth Exclamation Ma..Fullwidth Right White Pa
{0x0ffe0, 0x0ffe6}, // Fullwidth Cent Sign ..Fullwidth Won Sign {0x0ffe0, 0x0ffe6}, // Fullwidth Cent Sign ..Fullwidth Won Sign
{0x16fe0, 0x16fe4}, // Tangut Iteration Mark ..(nil) {0x16fe0, 0x16fe4}, // Tangut Iteration Mark ..Khitan Small Script Fill
{0x16ff0, 0x16ff1}, // (nil) ..(nil) {0x16ff0, 0x16ff1}, // Vietnamese Alternate Rea..Vietnamese Alternate Rea
{0x17000, 0x187f7}, // (nil) ..(nil) {0x17000, 0x187f7}, // (nil) ..(nil)
{0x18800, 0x18cd5}, // Tangut Component-001 ..(nil) {0x18800, 0x18cd5}, // Tangut Component-001 ..Khitan Small Script Char
{0x18d00, 0x18d08}, // (nil) ..(nil) {0x18d00, 0x18d08}, // (nil) ..(nil)
{0x1b000, 0x1b11e}, // Katakana Letter Archaic ..Hentaigana Letter N-mu-m {0x1aff0, 0x1aff3}, // Katakana Letter Minnan T..Katakana Letter Minnan T
{0x1aff5, 0x1affb}, // Katakana Letter Minnan T..Katakana Letter Minnan N
{0x1affd, 0x1affe}, // Katakana Letter Minnan N..Katakana Letter Minnan N
{0x1b000, 0x1b122}, // Katakana Letter Archaic ..Katakana Letter Archaic
{0x1b132, 0x1b132}, // (nil) ..(nil)
{0x1b150, 0x1b152}, // Hiragana Letter Small Wi..Hiragana Letter Small Wo {0x1b150, 0x1b152}, // Hiragana Letter Small Wi..Hiragana Letter Small Wo
{0x1b155, 0x1b155}, // (nil) ..(nil)
{0x1b164, 0x1b167}, // Katakana Letter Small Wi..Katakana Letter Small N {0x1b164, 0x1b167}, // Katakana Letter Small Wi..Katakana Letter Small N
{0x1b170, 0x1b2fb}, // Nushu Character-1b170 ..Nushu Character-1b2fb {0x1b170, 0x1b2fb}, // Nushu Character-1b170 ..Nushu Character-1b2fb
{0x1f004, 0x1f004}, // Mahjong Tile Red Dragon ..Mahjong Tile Red Dragon {0x1f004, 0x1f004}, // Mahjong Tile Red Dragon ..Mahjong Tile Red Dragon
@@ -443,24 +469,24 @@ public final class WcWidth {
{0x1f680, 0x1f6c5}, // Rocket ..Left Luggage {0x1f680, 0x1f6c5}, // Rocket ..Left Luggage
{0x1f6cc, 0x1f6cc}, // Sleeping Accommodation ..Sleeping Accommodation {0x1f6cc, 0x1f6cc}, // Sleeping Accommodation ..Sleeping Accommodation
{0x1f6d0, 0x1f6d2}, // Place Of Worship ..Shopping Trolley {0x1f6d0, 0x1f6d2}, // Place Of Worship ..Shopping Trolley
{0x1f6d5, 0x1f6d7}, // Hindu Temple ..(nil) {0x1f6d5, 0x1f6d7}, // Hindu Temple ..Elevator
{0x1f6dc, 0x1f6df}, // (nil) ..Ring Buoy
{0x1f6eb, 0x1f6ec}, // Airplane Departure ..Airplane Arriving {0x1f6eb, 0x1f6ec}, // Airplane Departure ..Airplane Arriving
{0x1f6f4, 0x1f6fc}, // Scooter ..(nil) {0x1f6f4, 0x1f6fc}, // Scooter ..Roller Skate
{0x1f7e0, 0x1f7eb}, // Large Orange Circle ..Large Brown Square {0x1f7e0, 0x1f7eb}, // Large Orange Circle ..Large Brown Square
{0x1f90c, 0x1f93a}, // (nil) ..Fencer {0x1f7f0, 0x1f7f0}, // Heavy Equals Sign ..Heavy Equals Sign
{0x1f90c, 0x1f93a}, // Pinched Fingers ..Fencer
{0x1f93c, 0x1f945}, // Wrestlers ..Goal Net {0x1f93c, 0x1f945}, // Wrestlers ..Goal Net
{0x1f947, 0x1f978}, // First Place Medal ..(nil) {0x1f947, 0x1f9ff}, // First Place Medal ..Nazar Amulet
{0x1f97a, 0x1f9cb}, // Face With Pleading Eyes ..(nil) {0x1fa70, 0x1fa7c}, // Ballet Shoes ..Crutch
{0x1f9cd, 0x1f9ff}, // Standing Person ..Nazar Amulet {0x1fa80, 0x1fa88}, // Yo-yo ..(nil)
{0x1fa70, 0x1fa74}, // Ballet Shoes ..(nil) {0x1fa90, 0x1fabd}, // Ringed Planet ..(nil)
{0x1fa78, 0x1fa7a}, // Drop Of Blood ..Stethoscope {0x1fabf, 0x1fac5}, // (nil) ..Person With Crown
{0x1fa80, 0x1fa86}, // Yo-yo ..(nil) {0x1face, 0x1fadb}, // (nil) ..(nil)
{0x1fa90, 0x1faa8}, // Ringed Planet ..(nil) {0x1fae0, 0x1fae8}, // Melting Face ..(nil)
{0x1fab0, 0x1fab6}, // (nil) ..(nil) {0x1faf0, 0x1faf8}, // Hand With Index Finger A..(nil)
{0x1fac0, 0x1fac2}, // (nil) ..(nil)
{0x1fad0, 0x1fad6}, // (nil) ..(nil)
{0x20000, 0x2fffd}, // Cjk Unified Ideograph-20..(nil) {0x20000, 0x2fffd}, // Cjk Unified Ideograph-20..(nil)
{0x30000, 0x3fffd}, // (nil) ..(nil) {0x30000, 0x3fffd}, // Cjk Unified Ideograph-30..(nil)
}; };
@@ -512,4 +538,29 @@ public final class WcWidth {
return Character.isHighSurrogate(c) ? width(Character.toCodePoint(c, chars[index + 1])) : width(c); return Character.isHighSurrogate(c) ? width(Character.toCodePoint(c, chars[index + 1])) : width(c);
} }
/**
* The zero width characters count like combining characters in the `chars` array from start
* index to end index (exclusive).
*/
public static int zeroWidthCharsCount(char[] chars, int start, int end) {
if (start < 0 || start >= chars.length)
return 0;
int count = 0;
for (int i = start; i < end && i < chars.length;) {
if (Character.isHighSurrogate(chars[i])) {
if (width(Character.toCodePoint(chars[i], chars[i + 1])) <= 0) {
count++;
}
i += 2;
} else {
if (width(chars[i]) <= 0) {
count++;
}
i++;
}
}
return count;
}
} }

View File

@@ -45,4 +45,21 @@ public class ScreenBufferTest extends TerminalTestCase {
withTerminalSized(5, 3).enterString("ABC\r\nFG"); withTerminalSized(5, 3).enterString("ABC\r\nFG");
assertEquals("ABC\nFG", mTerminal.getScreen().getSelectedText(0, 0, 1, 1, true, true)); assertEquals("ABC\nFG", mTerminal.getScreen().getSelectedText(0, 0, 1, 1, true, true));
} }
public void testGetWordAtLocation() {
withTerminalSized(5, 3).enterString("ABCDEFGHIJ\r\nKLMNO");
assertEquals("ABCDEFGHIJKLMNO", mTerminal.getScreen().getWordAtLocation(0, 0));
assertEquals("ABCDEFGHIJKLMNO", mTerminal.getScreen().getWordAtLocation(4, 1));
assertEquals("ABCDEFGHIJKLMNO", mTerminal.getScreen().getWordAtLocation(4, 2));
withTerminalSized(5, 3).enterString("ABC DEF GHI ");
assertEquals("ABC", mTerminal.getScreen().getWordAtLocation(0, 0));
assertEquals("", mTerminal.getScreen().getWordAtLocation(3, 0));
assertEquals("DEF", mTerminal.getScreen().getWordAtLocation(4, 0));
assertEquals("DEF", mTerminal.getScreen().getWordAtLocation(0, 1));
assertEquals("DEF", mTerminal.getScreen().getWordAtLocation(1, 1));
assertEquals("GHI", mTerminal.getScreen().getWordAtLocation(0, 2));
assertEquals("", mTerminal.getScreen().getWordAtLocation(1, 2));
assertEquals("", mTerminal.getScreen().getWordAtLocation(2, 2));
}
} }

View File

@@ -151,6 +151,19 @@ public class TerminalTest extends TerminalTestCase {
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor); assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor); assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
// Test CSI resetting to default if sequence starts with ; or has sequential ;;
// Check TerminalEmulator.parseArg()
enterString("\033[31m\033[m");
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
enterString("\033[31m\033[;m");
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");
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
enterString("\033[31;;m");
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
// 256 colors: // 256 colors:
enterString("\033[38;5;119m"); enterString("\033[38;5;119m");
assertEquals(119, mTerminal.mForeColor); assertEquals(119, mTerminal.mForeColor);

View File

@@ -37,10 +37,14 @@ public abstract class TerminalTestCase extends TestCase {
} }
@Override @Override
public void clipboardText(String text) { public void onCopyTextToClipboard(String text) {
clipboardPuts.add(text); clipboardPuts.add(text);
} }
@Override
public void onPasteTextFromClipboard() {
}
@Override @Override
public void onBell() { public void onBell() {
bellsRung++; bellsRung++;

View File

@@ -5,7 +5,7 @@ android {
compileSdkVersion project.properties.compileSdkVersion.toInteger() compileSdkVersion project.properties.compileSdkVersion.toInteger()
dependencies { dependencies {
implementation "androidx.annotation:annotation:1.2.0" implementation "androidx.annotation:annotation:1.3.0"
api project(":terminal-emulator") api project(":terminal-emulator")
} }
@@ -45,7 +45,7 @@ afterEvaluate {
from components.release from components.release
groupId = 'com.termux' groupId = 'com.termux'
artifactId = 'terminal-view' artifactId = 'terminal-view'
version = '0.115' version = '0.118.0'
artifact(sourceJar) artifact(sourceJar)
} }
} }

View File

@@ -118,9 +118,13 @@ public final class TerminalRenderer {
final int columnWidthSinceLastRun = column - lastRunStartColumn; final int columnWidthSinceLastRun = column - lastRunStartColumn;
final int charsSinceLastRun = currentCharIndex - lastRunStartIndex; final int charsSinceLastRun = currentCharIndex - lastRunStartIndex;
int cursorColor = lastRunInsideCursor ? mEmulator.mColors.mCurrentColors[TextStyle.COLOR_INDEX_CURSOR] : 0; int cursorColor = lastRunInsideCursor ? mEmulator.mColors.mCurrentColors[TextStyle.COLOR_INDEX_CURSOR] : 0;
boolean invertCursorTextColor = false;
if (lastRunInsideCursor && cursorShape == TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK) {
invertCursorTextColor = true;
}
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun,
lastRunStartIndex, charsSinceLastRun, measuredWidthForRun, lastRunStartIndex, charsSinceLastRun, measuredWidthForRun,
cursorColor, cursorShape, lastRunStyle, reverseVideo || lastRunInsideSelection); cursorColor, cursorShape, lastRunStyle, reverseVideo || invertCursorTextColor || lastRunInsideSelection);
} }
measuredWidthForRun = 0.f; measuredWidthForRun = 0.f;
lastRunStyle = style; lastRunStyle = style;
@@ -143,8 +147,12 @@ public final class TerminalRenderer {
final int columnWidthSinceLastRun = columns - lastRunStartColumn; final int columnWidthSinceLastRun = columns - lastRunStartColumn;
final int charsSinceLastRun = currentCharIndex - lastRunStartIndex; final int charsSinceLastRun = currentCharIndex - lastRunStartIndex;
int cursorColor = lastRunInsideCursor ? mEmulator.mColors.mCurrentColors[TextStyle.COLOR_INDEX_CURSOR] : 0; int cursorColor = lastRunInsideCursor ? mEmulator.mColors.mCurrentColors[TextStyle.COLOR_INDEX_CURSOR] : 0;
boolean invertCursorTextColor = false;
if (lastRunInsideCursor && cursorShape == TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK) {
invertCursorTextColor = true;
}
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun, drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun,
measuredWidthForRun, cursorColor, cursorShape, lastRunStyle, reverseVideo || lastRunInsideSelection); measuredWidthForRun, cursorColor, cursorShape, lastRunStyle, reverseVideo || invertCursorTextColor || lastRunInsideSelection);
} }
} }

View File

@@ -94,7 +94,7 @@ public final class TerminalView extends View {
@Override @Override
public boolean onUp(MotionEvent event) { public boolean onUp(MotionEvent event) {
mScrollRemainder = 0.0f; mScrollRemainder = 0.0f;
if (mEmulator != null && mEmulator.isMouseTrackingActive() && !isSelectingText() && !scrolledWithFinger) { if (mEmulator != null && mEmulator.isMouseTrackingActive() && !event.isFromSource(InputDevice.SOURCE_MOUSE) && !isSelectingText() && !scrolledWithFinger) {
// Quick event processing when mouse tracking is active - do not wait for check of double tapping // Quick event processing when mouse tracking is active - do not wait for check of double tapping
// for zooming. // for zooming.
sendMouseEventCode(event, TerminalEmulator.MOUSE_LEFT_BUTTON, true); sendMouseEventCode(event, TerminalEmulator.MOUSE_LEFT_BUTTON, true);
@@ -114,13 +114,8 @@ public final class TerminalView extends View {
return true; return true;
} }
requestFocus(); requestFocus();
if (!mEmulator.isMouseTrackingActive()) { mClient.onSingleTapUp(event);
if (!event.isFromSource(InputDevice.SOURCE_MOUSE)) { return true;
mClient.onSingleTapUp(event);
return true;
}
}
return false;
} }
@Override @Override
@@ -262,20 +257,33 @@ public final class TerminalView extends View {
@Override @Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) { public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
if (mClient.shouldEnforceCharBasedInput()) { // Ensure that inputType is only set if TerminalView is selected view with the keyboard and
// Some keyboards seems do not reset the internal state on TYPE_NULL. // an alternate view is not selected, like an EditText. This is necessary if an activity is
// Affects mostly Samsung stock keyboards. // initially started with the alternate view or if activity is returned to from another app
// https://github.com/termux/termux-app/issues/686 // and the alternate view was the one selected the last time.
outAttrs.inputType = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS; if (mClient.isTerminalViewSelected()) {
if (mClient.shouldEnforceCharBasedInput()) {
// Some keyboards seems do not reset the internal state on TYPE_NULL.
// Affects mostly Samsung stock keyboards.
// https://github.com/termux/termux-app/issues/686
// However, this is not a valid value as per AOSP since `InputType.TYPE_CLASS_*` is
// not set and it logs a warning:
// W/InputAttributes: Unexpected input class: inputType=0x00080090 imeOptions=0x02000000
// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:packages/inputmethods/LatinIME/java/src/com/android/inputmethod/latin/InputAttributes.java;l=79
outAttrs.inputType = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
} else {
// Using InputType.NULL is the most correct input type and avoids issues with other hacks.
//
// Previous keyboard issues:
// https://github.com/termux/termux-packages/issues/25
// https://github.com/termux/termux-app/issues/87.
// https://github.com/termux/termux-app/issues/126.
// https://github.com/termux/termux-app/issues/137 (japanese chars and TYPE_NULL).
outAttrs.inputType = InputType.TYPE_NULL;
}
} else { } else {
// Using InputType.NULL is the most correct input type and avoids issues with other hacks. // Corresponds to android:inputType="text"
// outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL;
// Previous keyboard issues:
// https://github.com/termux/termux-packages/issues/25
// https://github.com/termux/termux-app/issues/87.
// https://github.com/termux/termux-app/issues/126.
// https://github.com/termux/termux-app/issues/137 (japanese chars and TYPE_NULL).
outAttrs.inputType = InputType.TYPE_NULL;
} }
// Note that IME_ACTION_NONE cannot be used as that makes it impossible to input newlines using the on-screen // Note that IME_ACTION_NONE cannot be used as that makes it impossible to input newlines using the on-screen
@@ -337,6 +345,10 @@ public final class TerminalView extends View {
codePoint = firstChar; codePoint = firstChar;
} }
// Check onKeyDown() for details.
if (mClient.readShiftKey())
codePoint = Character.toUpperCase(codePoint);
boolean ctrlHeld = false; boolean ctrlHeld = false;
if (codePoint <= 31 && codePoint != 27) { if (codePoint <= 31 && codePoint != 27) {
if (codePoint == '\n') { if (codePoint == '\n') {
@@ -454,10 +466,31 @@ public final class TerminalView extends View {
return true; return true;
} }
/**
* Get the zero indexed column and row of the terminal view for the
* position of the event.
*
* @param event The event with the position to get the column and row for.
* @param relativeToScroll If true the column number will take the scroll
* position into account. E.g. if scrolled 3 lines up and the event
* position is in the top left, column will be -3 if relativeToScroll is
* true and 0 if relativeToScroll is false.
* @return Array with the column and row.
*/
public int[] getColumnAndRow(MotionEvent event, boolean relativeToScroll) {
int column = (int) (event.getX() / mRenderer.mFontWidth);
int row = (int) ((event.getY() - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing);
if (relativeToScroll) {
row += mTopRow;
}
return new int[] { column, row };
}
/** Send a single mouse event code to the terminal. */ /** Send a single mouse event code to the terminal. */
void sendMouseEventCode(MotionEvent e, int button, boolean pressed) { void sendMouseEventCode(MotionEvent e, int button, boolean pressed) {
int x = (int) (e.getX() / mRenderer.mFontWidth) + 1; int[] columnAndRow = getColumnAndRow(e, false);
int y = (int) ((e.getY() - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing) + 1; int x = columnAndRow[0] + 1;
int y = columnAndRow[1] + 1;
if (pressed && (button == TerminalEmulator.MOUSE_WHEELDOWN_BUTTON || button == TerminalEmulator.MOUSE_WHEELUP_BUTTON)) { if (pressed && (button == TerminalEmulator.MOUSE_WHEELDOWN_BUTTON || button == TerminalEmulator.MOUSE_WHEELUP_BUTTON)) {
if (mMouseStartDownTime == e.getDownTime()) { if (mMouseStartDownTime == e.getDownTime()) {
x = mMouseScrollStartX; x = mMouseScrollStartX;
@@ -533,7 +566,6 @@ public final class TerminalView extends View {
sendMouseEventCode(event, TerminalEmulator.MOUSE_LEFT_BUTTON_MOVED, true); sendMouseEventCode(event, TerminalEmulator.MOUSE_LEFT_BUTTON_MOVED, true);
break; break;
} }
return true;
} }
} }
@@ -567,6 +599,102 @@ public final class TerminalView extends View {
return super.onKeyPreIme(keyCode, event); return super.onKeyPreIme(keyCode, event);
} }
/**
* Key presses in software keyboards will generally NOT trigger this listener, although some
* may elect to do so in some situations. Do not rely on this to catch software key presses.
* Gboard calls this when shouldEnforceCharBasedInput() is disabled (InputType.TYPE_NULL) instead
* of calling commitText(), with deviceId=-1. However, Hacker's Keyboard, OpenBoard, LG Keyboard
* call commitText().
*
* This function may also be called directly without android calling it, like by
* `TerminalExtraKeys` which generates a KeyEvent manually which uses {@link KeyCharacterMap#VIRTUAL_KEYBOARD}
* as the device (deviceId=-1), as does Gboard. That would normally use mappings defined in
* `/system/usr/keychars/Virtual.kcm`. You can run `dumpsys input` to find the `KeyCharacterMapFile`
* used by virtual keyboard or hardware keyboard. Note that virtual keyboard device is not the
* same as software keyboard, like Gboard, etc. Its a fake device used for generating events and
* for testing.
*
* We handle shift key in `commitText()` to convert codepoint to uppercase case there with a
* call to {@link Character#toUpperCase(int)}, but here we instead rely on getUnicodeChar() for
* conversion of keyCode, for both hardware keyboard shift key (via effectiveMetaState) and
* `mClient.readShiftKey()`, based on value in kcm files.
* This may result in different behaviour depending on keyboard and android kcm files set for the
* InputDevice for the event passed to this function. This will likely be an issue for non-english
* languages since `Virtual.kcm` in english only by default or at least in AOSP. For both hardware
* shift key (via effectiveMetaState) and `mClient.readShiftKey()`, `getUnicodeChar()` is used
* for shift specific behaviour which usually is to uppercase.
*
* For fn key on hardware keyboard, android checks kcm files for hardware keyboards, which is
* `Generic.kcm` by default, unless a vendor specific one is defined. The event passed will have
* {@link KeyEvent#META_FUNCTION_ON} set. If the kcm file only defines a single character or unicode
* code point `\\uxxxx`, then only one event is passed with that value. However, if kcm defines
* a `fallback` key for fn or others, like `key DPAD_UP { ... fn: fallback PAGE_UP }`, then
* android will first pass an event with original key `DPAD_UP` and {@link KeyEvent#META_FUNCTION_ON}
* set. But this function will not consume it and android will pass another event with `PAGE_UP`
* and {@link KeyEvent#META_FUNCTION_ON} not set, which will be consumed.
*
* Now there are some other issues as well, firstly ctrl and alt flags are not passed to
* `getUnicodeChar()`, so modified key values in kcm are not used. Secondly, if the kcm file
* for other modifiers like shift or fn define a non-alphabet, like { fn: '\u0015' } to act as
* DPAD_LEFT, the `getUnicodeChar()` will correctly return `21` as the code point but action will
* not happen because the `handleKeyCode()` function that transforms DPAD_LEFT to `\033[D`
* escape sequence for the terminal to perform the left action would not be called since its
* called before `getUnicodeChar()` and terminal will instead get `21 0x15 Negative Acknowledgement`.
* The solution to such issues is calling `getUnicodeChar()` before the call to `handleKeyCode()`
* if user has defined a custom kcm file, like done in POC mentioned in #2237. Note that
* Hacker's Keyboard calls `commitText()` so don't test fn/shift with it for this function.
* https://github.com/termux/termux-app/pull/2237
* https://github.com/agnostic-apollo/termux-app/blob/terminal-code-point-custom-mapping/terminal-view/src/main/java/com/termux/view/TerminalView.java
*
* Key Character Map (kcm) and Key Layout (kl) files info:
* https://source.android.com/devices/input/key-character-map-files
* https://source.android.com/devices/input/key-layout-files
* https://source.android.com/devices/input/keyboard-devices
* AOSP kcm and kl files:
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/data/keyboards
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/packages/InputDevices/res/raw
*
* KeyCodes:
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/core/java/android/view/KeyEvent.java
* https://cs.android.com/android/platform/superproject/+/master:frameworks/native/include/android/keycodes.h
*
* `dumpsys input`:
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/services/inputflinger/reader/EventHub.cpp;l=1917
*
* Loading of keymap:
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/services/inputflinger/reader/EventHub.cpp;l=1644
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/libs/input/Keyboard.cpp;l=41
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/libs/input/InputDevice.cpp
* OVERLAY keymaps for hardware keyboards may be combined as well:
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/libs/input/KeyCharacterMap.cpp;l=165
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/libs/input/KeyCharacterMap.cpp;l=831
*
* Parse kcm file:
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/libs/input/KeyCharacterMap.cpp;l=727
* Parse key value:
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/libs/input/KeyCharacterMap.cpp;l=981
*
* `KeyEvent.getUnicodeChar()`
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/core/java/android/view/KeyEvent.java;l=2716
* https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/KeyCharacterMap.java;l=368
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/core/jni/android_view_KeyCharacterMap.cpp;l=117
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/libs/input/KeyCharacterMap.cpp;l=231
*
* Keyboard layouts advertised by applications, like for hardware keyboards via #ACTION_QUERY_KEYBOARD_LAYOUTS
* Config is stored in `/data/system/input-manager-state.xml`
* https://github.com/ris58h/custom-keyboard-layout
* Loading from apps:
* https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/input/InputManagerService.java;l=1221
* Set:
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/core/java/android/hardware/input/InputManager.java;l=89
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/core/java/android/hardware/input/InputManager.java;l=543
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:packages/apps/Settings/src/com/android/settings/inputmethod/KeyboardLayoutDialogFragment.java;l=167
* https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/input/InputManagerService.java;l=1385
* https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/input/PersistentDataStore.java
* Get overlay keyboard layout
* https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/input/InputManagerService.java;l=2158
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp;l=616
*/
@Override @Override
public boolean onKeyDown(int keyCode, KeyEvent event) { public boolean onKeyDown(int keyCode, KeyEvent event) {
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) if (TERMINAL_VIEW_KEY_LOGGING_ENABLED)
@@ -589,13 +717,15 @@ public final class TerminalView extends View {
final int metaState = event.getMetaState(); final int metaState = event.getMetaState();
final boolean controlDown = event.isCtrlPressed() || mClient.readControlKey(); final boolean controlDown = event.isCtrlPressed() || mClient.readControlKey();
final boolean leftAltDown = (metaState & KeyEvent.META_ALT_LEFT_ON) != 0 || mClient.readAltKey(); final boolean leftAltDown = (metaState & KeyEvent.META_ALT_LEFT_ON) != 0 || mClient.readAltKey();
final boolean shiftDown = event.isShiftPressed() || mClient.readShiftKey();
final boolean rightAltDownFromEvent = (metaState & KeyEvent.META_ALT_RIGHT_ON) != 0; final boolean rightAltDownFromEvent = (metaState & KeyEvent.META_ALT_RIGHT_ON) != 0;
int keyMod = 0; int keyMod = 0;
if (controlDown) keyMod |= KeyHandler.KEYMOD_CTRL; if (controlDown) keyMod |= KeyHandler.KEYMOD_CTRL;
if (event.isAltPressed() || leftAltDown) keyMod |= KeyHandler.KEYMOD_ALT; if (event.isAltPressed() || leftAltDown) keyMod |= KeyHandler.KEYMOD_ALT;
if (event.isShiftPressed()) keyMod |= KeyHandler.KEYMOD_SHIFT; if (shiftDown) keyMod |= KeyHandler.KEYMOD_SHIFT;
if (event.isNumLockOn()) keyMod |= KeyHandler.KEYMOD_NUM_LOCK; if (event.isNumLockOn()) keyMod |= KeyHandler.KEYMOD_NUM_LOCK;
// https://github.com/termux/termux-app/issues/731
if (!event.isFunctionPressed() && handleKeyCode(keyCode, keyMod)) { if (!event.isFunctionPressed() && handleKeyCode(keyCode, keyMod)) {
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) mClient.logInfo(LOG_TAG, "handleKeyCode() took key event"); if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) mClient.logInfo(LOG_TAG, "handleKeyCode() took key event");
return true; return true;
@@ -611,6 +741,9 @@ public final class TerminalView extends View {
} }
int effectiveMetaState = event.getMetaState() & ~bitsToClear; int effectiveMetaState = event.getMetaState() & ~bitsToClear;
if (shiftDown) effectiveMetaState |= KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_LEFT_ON;
if (mClient.readFnKey()) effectiveMetaState |= KeyEvent.META_FUNCTION_ON;
int result = event.getUnicodeChar(effectiveMetaState); int result = event.getUnicodeChar(effectiveMetaState);
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) if (TERMINAL_VIEW_KEY_LOGGING_ENABLED)
mClient.logInfo(LOG_TAG, "KeyEvent#getUnicodeChar(" + effectiveMetaState + ") returned: " + result); mClient.logInfo(LOG_TAG, "KeyEvent#getUnicodeChar(" + effectiveMetaState + ") returned: " + result);
@@ -646,6 +779,10 @@ public final class TerminalView extends View {
if (mTermSession == null) return; if (mTermSession == null) return;
// Ensure cursor is shown when a key is pressed down like long hold on (arrow) keys
if (mEmulator != null)
mEmulator.setCursorBlinkState(true);
final boolean controlDown = controlDownFromEvent || mClient.readControlKey(); final boolean controlDown = controlDownFromEvent || mClient.readControlKey();
final boolean altDown = leftAltDownFromEvent || mClient.readAltKey(); final boolean altDown = leftAltDownFromEvent || mClient.readAltKey();

View File

@@ -34,6 +34,8 @@ public interface TerminalViewClient {
boolean shouldUseCtrlSpaceWorkaround(); boolean shouldUseCtrlSpaceWorkaround();
boolean isTerminalViewSelected();
void copyModeChanged(boolean copyMode); void copyModeChanged(boolean copyMode);
@@ -52,6 +54,11 @@ public interface TerminalViewClient {
boolean readAltKey(); boolean readAltKey();
boolean readShiftKey();
boolean readFnKey();
boolean onCodePoint(int codePoint, boolean ctrlDown, TerminalSession session); boolean onCodePoint(int codePoint, boolean ctrlDown, TerminalSession session);

View File

@@ -89,14 +89,9 @@ public class TextSelectionCursorController implements CursorController {
} }
public void setInitialTextSelectionPosition(MotionEvent event) { public void setInitialTextSelectionPosition(MotionEvent event) {
int cx = (int) (event.getX() / terminalView.mRenderer.getFontWidth()); int[] columnAndRow = terminalView.getColumnAndRow(event, true);
final boolean eventFromMouse = event.isFromSource(InputDevice.SOURCE_MOUSE); mSelX1 = mSelX2 = columnAndRow[0];
// Offset for finger: mSelY1 = mSelY2 = columnAndRow[1];
final int SELECT_TEXT_OFFSET_Y = eventFromMouse ? 0 : -40;
int cy = (int) ((event.getY() + SELECT_TEXT_OFFSET_Y) / terminalView.mRenderer.getFontLineSpacing()) + terminalView.getTopRow();
mSelX1 = mSelX2 = cx;
mSelY1 = mSelY2 = cy;
TerminalBuffer screen = terminalView.mEmulator.getScreen(); TerminalBuffer screen = terminalView.mEmulator.getScreen();
if (!" ".equals(screen.getSelectedText(mSelX1, mSelY1, mSelX1, mSelY1))) { if (!" ".equals(screen.getSelectedText(mSelX1, mSelY1, mSelX1, mSelY1))) {
@@ -138,17 +133,12 @@ public class TextSelectionCursorController implements CursorController {
switch (item.getItemId()) { switch (item.getItemId()) {
case ACTION_COPY: case ACTION_COPY:
String selectedText = terminalView.mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2).trim(); String selectedText = terminalView.mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2).trim();
terminalView.mTermSession.clipboardText(selectedText); terminalView.mTermSession.onCopyTextToClipboard(selectedText);
terminalView.stopTextSelectionMode(); terminalView.stopTextSelectionMode();
break; break;
case ACTION_PASTE: case ACTION_PASTE:
terminalView.stopTextSelectionMode(); terminalView.stopTextSelectionMode();
ClipboardManager clipboard = (ClipboardManager) terminalView.getContext().getSystemService(Context.CLIPBOARD_SERVICE); terminalView.mTermSession.onPasteTextFromClipboard();
ClipData clipData = clipboard.getPrimaryClip();
if (clipData != null) {
CharSequence paste = clipData.getItemAt(0).coerceToText(terminalView.getContext());
if (!TextUtils.isEmpty(paste)) terminalView.mEmulator.paste(paste.toString());
}
break; break;
case ACTION_MORE: case ACTION_MORE:
terminalView.stopTextSelectionMode(); //we stop text selection first, otherwise handles will show above popup terminalView.stopTextSelectionMode(); //we stop text selection first, otherwise handles will show above popup
@@ -193,14 +183,19 @@ public class TextSelectionCursorController implements CursorController {
int y1 = Math.round((mSelY1 - 1 - terminalView.getTopRow()) * terminalView.mRenderer.getFontLineSpacing()); int y1 = Math.round((mSelY1 - 1 - terminalView.getTopRow()) * terminalView.mRenderer.getFontLineSpacing());
int y2 = Math.round((mSelY2 + 1 - terminalView.getTopRow()) * terminalView.mRenderer.getFontLineSpacing()); int y2 = Math.round((mSelY2 + 1 - terminalView.getTopRow()) * terminalView.mRenderer.getFontLineSpacing());
if (x1 > x2) { if (x1 > x2) {
int tmp = x1; int tmp = x1;
x1 = x2; x1 = x2;
x2 = tmp; x2 = tmp;
} }
outRect.set(x1, y1 + mHandleHeight, x2, y2 + mHandleHeight); int terminalBottom = terminalView.getBottom();
int top = y1 + mHandleHeight;
int bottom = y2 + mHandleHeight;
if (top > terminalBottom) top = terminalBottom;
if (bottom > terminalBottom) bottom = terminalBottom;
outRect.set(x1, top, x2, bottom);
} }
}, ActionMode.TYPE_FLOATING); }, ActionMode.TYPE_FLOATING);
} }

View File

@@ -7,7 +7,7 @@ The `termux-shared` library is released under [GPLv3 only](https://www.gnu.org/l
- [`src/main/java/com/termux/shared/termux/TermuxConstants.java`](src/main/java/com/termux/shared/termux/TermuxConstants.java). - [`src/main/java/com/termux/shared/termux/TermuxConstants.java`](src/main/java/com/termux/shared/termux/TermuxConstants.java).
- [`src/main/java/com/termux/shared/settings/properties/TermuxPropertyConstants.java`](src/main/java/com/termux/shared/settings/properties/TermuxPropertyConstants.java). - [`src/main/java/com/termux/shared/settings/properties/TermuxPropertyConstants.java`](src/main/java/com/termux/shared/settings/properties/TermuxPropertyConstants.java).
- [`src/main/java/com/termux/shared/activities/ReportActivity.java`](src/main/java/com/termux/shared/activities/ReportActivity.java). - [`src/main/java/com/termux/shared/activities/*`](src/main/java/com/termux/shared/activities).
- [`src/main/java/com/termux/shared/crash/CrashHandler.java`](src/main/java/com/termux/shared/crash/CrashHandler.java). - [`src/main/java/com/termux/shared/crash/CrashHandler.java`](src/main/java/com/termux/shared/crash/CrashHandler.java).

View File

@@ -5,16 +5,20 @@ android {
compileSdkVersion project.properties.compileSdkVersion.toInteger() compileSdkVersion project.properties.compileSdkVersion.toInteger()
dependencies { dependencies {
implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'androidx.appcompat:appcompat:1.3.1'
implementation "androidx.annotation:annotation:1.2.0" implementation "androidx.annotation:annotation:1.3.0"
implementation "androidx.core:core:1.6.0-rc01" implementation "androidx.core:core:1.6.0"
implementation "androidx.window:window:1.0.0-alpha08" implementation 'com.google.android.material:material:1.4.0'
implementation "com.google.guava:guava:24.1-jre" implementation "com.google.guava:guava:24.1-jre"
implementation "io.noties.markwon:core:$markwonVersion" implementation "io.noties.markwon:core:$markwonVersion"
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion" implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
implementation "io.noties.markwon:linkify:$markwonVersion" implementation "io.noties.markwon:linkify:$markwonVersion"
implementation "io.noties.markwon:recycler:$markwonVersion" implementation "io.noties.markwon:recycler:$markwonVersion"
// Do not increment version higher than 1.0.0-alpha09 since it will break ViewUtils and needs to be looked into
// noinspection GradleDependency
implementation "androidx.window:window:1.0.0-alpha09"
// Do not increment version higher than 2.5 or there // Do not increment version higher than 2.5 or there
// will be runtime exceptions on android < 8 // will be runtime exceptions on android < 8
// due to missing classes like java.nio.file.Path. // due to missing classes like java.nio.file.Path.
@@ -44,8 +48,8 @@ android {
dependencies { dependencies {
testImplementation "junit:junit:4.13.2" testImplementation "junit:junit:4.13.2"
androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
} }
task sourceJar(type: Jar) { task sourceJar(type: Jar) {
@@ -61,7 +65,7 @@ afterEvaluate {
from components.release from components.release
groupId = 'com.termux' groupId = 'com.termux'
artifactId = 'termux-shared' artifactId = 'termux-shared'
version = '0.115' version = '0.118.0'
artifact(sourceJar) artifact(sourceJar)
} }
} }

View File

@@ -1,3 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest package="com.termux.shared"> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.termux.shared">
<uses-permission android:name="android.permission.VIBRATE" />
</manifest> </manifest>

View File

@@ -7,36 +7,71 @@ import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle; import android.os.Bundle;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import com.termux.shared.R; import com.termux.shared.R;
import com.termux.shared.data.DataUtils;
import com.termux.shared.file.FileUtils;
import com.termux.shared.file.filesystem.FileType;
import com.termux.shared.logger.Logger;
import com.termux.shared.models.errors.Error;
import com.termux.shared.termux.TermuxConstants; import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.markdown.MarkdownUtils; import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.interact.ShareUtils; import com.termux.shared.interact.ShareUtils;
import com.termux.shared.models.ReportInfo; import com.termux.shared.models.ReportInfo;
import org.commonmark.node.FencedCodeBlock; import org.commonmark.node.FencedCodeBlock;
import org.jetbrains.annotations.NotNull;
import io.noties.markwon.Markwon; import io.noties.markwon.Markwon;
import io.noties.markwon.recycler.MarkwonAdapter; import io.noties.markwon.recycler.MarkwonAdapter;
import io.noties.markwon.recycler.SimpleEntry; import io.noties.markwon.recycler.SimpleEntry;
/**
* An activity to show reports in markdown format as per CommonMark spec based on config passed as {@link ReportInfo}.
* Add Following to `AndroidManifest.xml` to use in an app:
* {@code `<activity android:name="com.termux.shared.activities.ReportActivity" android:theme="@style/Theme.AppCompat.TermuxReportActivity" android:documentLaunchMode="intoExisting" />` }
* and
* {@code `<receiver android:name="com.termux.shared.activities.ReportActivity$ReportActivityBroadcastReceiver" android:exported="false" />` }
* Receiver **must not** be `exported="true"`!!!
*
* Also make an incremental call to {@link #deleteReportInfoFilesOlderThanXDays(Context, int, boolean)}
* in the app to cleanup cached files.
*/
public class ReportActivity extends AppCompatActivity { public class ReportActivity extends AppCompatActivity {
private static final String EXTRA_REPORT_INFO = "report_info"; private static final String CLASS_NAME = ReportActivity.class.getCanonicalName();
private static final String ACTION_DELETE_REPORT_INFO_OBJECT_FILE = CLASS_NAME + ".ACTION_DELETE_REPORT_INFO_OBJECT_FILE";
ReportInfo mReportInfo; private static final String EXTRA_REPORT_INFO_OBJECT = CLASS_NAME + ".EXTRA_REPORT_INFO_OBJECT";
String mReportMarkdownString; private static final String EXTRA_REPORT_INFO_OBJECT_FILE_PATH = CLASS_NAME + ".EXTRA_REPORT_INFO_OBJECT_FILE_PATH";
String mReportActivityMarkdownString;
private static final String CACHE_DIR_BASENAME = "report_activity";
private static final String CACHE_FILE_BASENAME_PREFIX = "report_info_";
public static final int REQUEST_GRANT_STORAGE_PERMISSION_FOR_SAVE_FILE = 1000;
public static final int ACTIVITY_TEXT_SIZE_LIMIT_IN_BYTES = 1000 * 1024; // 1MB
private ReportInfo mReportInfo;
private String mReportInfoFilePath;
private String mReportActivityMarkdownString;
private Bundle mBundle;
private static final String LOG_TAG = "ReportActivity";
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
Logger.logVerbose(LOG_TAG, "onCreate");
setContentView(R.layout.activity_report); setContentView(R.layout.activity_report);
Toolbar toolbar = findViewById(R.id.toolbar); Toolbar toolbar = findViewById(R.id.toolbar);
@@ -44,38 +79,65 @@ public class ReportActivity extends AppCompatActivity {
setSupportActionBar(toolbar); setSupportActionBar(toolbar);
} }
Bundle bundle = null; mBundle = null;
Intent intent = getIntent(); Intent intent = getIntent();
if (intent != null) if (intent != null)
bundle = intent.getExtras(); mBundle = intent.getExtras();
else if (savedInstanceState != null) else if (savedInstanceState != null)
bundle = savedInstanceState; mBundle = savedInstanceState;
updateUI(bundle); updateUI();
} }
@Override @Override
protected void onNewIntent(Intent intent) { protected void onNewIntent(Intent intent) {
super.onNewIntent(intent); super.onNewIntent(intent);
Logger.logVerbose(LOG_TAG, "onNewIntent");
setIntent(intent); setIntent(intent);
if (intent != null) if (intent != null) {
updateUI(intent.getExtras()); deleteReportInfoFile(this, mReportInfoFilePath);
mBundle = intent.getExtras();
updateUI();
}
} }
private void updateUI(Bundle bundle) { private void updateUI() {
if (bundle == null) { if (mBundle == null) {
finish(); finish(); return;
return;
} }
mReportInfo = (ReportInfo) bundle.getSerializable(EXTRA_REPORT_INFO); mReportInfo = null;
mReportInfoFilePath = null;
if (mBundle.containsKey(EXTRA_REPORT_INFO_OBJECT_FILE_PATH)) {
mReportInfoFilePath = mBundle.getString(EXTRA_REPORT_INFO_OBJECT_FILE_PATH);
Logger.logVerbose(LOG_TAG, ReportInfo.class.getSimpleName() + " serialized object will be read from file at path \"" + mReportInfoFilePath + "\"");
if (mReportInfoFilePath != null) {
try {
FileUtils.ReadSerializableObjectResult result = FileUtils.readSerializableObjectFromFile(ReportInfo.class.getSimpleName(), mReportInfoFilePath, ReportInfo.class, false);
if (result.error != null) {
Logger.logErrorExtended(LOG_TAG, result.error.toString());
Logger.showToast(this, Error.getMinimalErrorString(result.error), true);
finish(); return;
} else {
if (result.serializableObject != null)
mReportInfo = (ReportInfo) result.serializableObject;
}
} catch (Exception e) {
Logger.logErrorAndShowToast(this, LOG_TAG, e.getMessage());
Logger.logStackTraceWithMessage(LOG_TAG, "Failure while getting " + ReportInfo.class.getSimpleName() + " serialized object from file at path \"" + mReportInfoFilePath + "\"", e);
}
}
} else {
mReportInfo = (ReportInfo) mBundle.getSerializable(EXTRA_REPORT_INFO_OBJECT);
}
if (mReportInfo == null) { if (mReportInfo == null) {
finish(); finish(); return;
return;
} }
@@ -99,25 +161,41 @@ public class ReportActivity extends AppCompatActivity {
recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(adapter); recyclerView.setAdapter(adapter);
generateReportActivityMarkdownString(); generateReportActivityMarkdownString();
adapter.setMarkdown(markwon, mReportActivityMarkdownString); adapter.setMarkdown(markwon, mReportActivityMarkdownString);
adapter.notifyDataSetChanged(); adapter.notifyDataSetChanged();
} }
@Override @Override
public void onSaveInstanceState(@NonNull Bundle outState) { public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
if (mBundle.containsKey(EXTRA_REPORT_INFO_OBJECT_FILE_PATH)) {
outState.putString(EXTRA_REPORT_INFO_OBJECT_FILE_PATH, mReportInfoFilePath);
} else {
outState.putSerializable(EXTRA_REPORT_INFO_OBJECT, mReportInfo);
}
}
outState.putSerializable(EXTRA_REPORT_INFO, mReportInfo); @Override
protected void onDestroy() {
super.onDestroy();
Logger.logVerbose(LOG_TAG, "onDestroy");
deleteReportInfoFile(this, mReportInfoFilePath);
} }
@Override @Override
public boolean onCreateOptionsMenu(final Menu menu) { public boolean onCreateOptionsMenu(final Menu menu) {
final MenuInflater inflater = getMenuInflater(); final MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_report, menu); inflater.inflate(R.menu.menu_report, menu);
if (mReportInfo.reportSaveFilePath == null) {
MenuItem item = menu.findItem(R.id.menu_item_save_report_to_file);
if (item != null)
item.setEnabled(false);
}
return true; return true;
} }
@@ -131,50 +209,264 @@ public class ReportActivity extends AppCompatActivity {
public boolean onOptionsItemSelected(final MenuItem item) { public boolean onOptionsItemSelected(final MenuItem item) {
int id = item.getItemId(); int id = item.getItemId();
if (id == R.id.menu_item_share_report) { if (id == R.id.menu_item_share_report) {
if (mReportMarkdownString != null) ShareUtils.shareText(this, getString(R.string.title_report_text), ReportInfo.getReportInfoMarkdownString(mReportInfo));
ShareUtils.shareText(this, getString(R.string.title_report_text), mReportMarkdownString);
} else if (id == R.id.menu_item_copy_report) { } else if (id == R.id.menu_item_copy_report) {
if (mReportMarkdownString != null) ShareUtils.copyTextToClipboard(this, ReportInfo.getReportInfoMarkdownString(mReportInfo), null);
ShareUtils.copyTextToClipboard(this, mReportMarkdownString, null); } else if (id == R.id.menu_item_save_report_to_file) {
ShareUtils.saveTextToFile(this, mReportInfo.reportSaveFileLabel,
mReportInfo.reportSaveFilePath, ReportInfo.getReportInfoMarkdownString(mReportInfo),
true, REQUEST_GRANT_STORAGE_PERMISSION_FOR_SAVE_FILE);
} }
return false; return false;
} }
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Logger.logInfo(LOG_TAG, "Storage permission granted by user on request.");
if (requestCode == REQUEST_GRANT_STORAGE_PERMISSION_FOR_SAVE_FILE) {
ShareUtils.saveTextToFile(this, mReportInfo.reportSaveFileLabel,
mReportInfo.reportSaveFilePath, ReportInfo.getReportInfoMarkdownString(mReportInfo),
true, -1);
}
} else {
Logger.logInfo(LOG_TAG, "Storage permission denied by user on request.");
}
}
/** /**
* Generate the markdown {@link String} to be shown in {@link ReportActivity}. * Generate the markdown {@link String} to be shown in {@link ReportActivity}.
*/ */
private void generateReportActivityMarkdownString() { private void generateReportActivityMarkdownString() {
mReportMarkdownString = ReportInfo.getReportInfoMarkdownString(mReportInfo); // We need to reduce chances of OutOfMemoryError happening so reduce new allocations and
// do not keep output of getReportInfoMarkdownString in memory
StringBuilder reportString = new StringBuilder();
mReportActivityMarkdownString = "";
if (mReportInfo.reportStringPrefix != null) if (mReportInfo.reportStringPrefix != null)
mReportActivityMarkdownString += mReportInfo.reportStringPrefix; reportString.append(mReportInfo.reportStringPrefix);
mReportActivityMarkdownString += mReportMarkdownString; String reportMarkdownString = ReportInfo.getReportInfoMarkdownString(mReportInfo);
int reportMarkdownStringSize = reportMarkdownString.getBytes().length;
boolean truncated = false;
if (reportMarkdownStringSize > ACTIVITY_TEXT_SIZE_LIMIT_IN_BYTES) {
Logger.logVerbose(LOG_TAG, mReportInfo.reportTitle + " report string size " + reportMarkdownStringSize + " is greater than " + ACTIVITY_TEXT_SIZE_LIMIT_IN_BYTES + " and will be truncated");
reportString.append(DataUtils.getTruncatedCommandOutput(reportMarkdownString, ACTIVITY_TEXT_SIZE_LIMIT_IN_BYTES, true, false, true));
truncated = true;
} else {
reportString.append(reportMarkdownString);
}
// Free reference
reportMarkdownString = null;
if (mReportInfo.reportStringSuffix != null) if (mReportInfo.reportStringSuffix != null)
mReportActivityMarkdownString += mReportInfo.reportStringSuffix; reportString.append(mReportInfo.reportStringSuffix);
int reportStringSize = reportString.length();
if (reportStringSize > ACTIVITY_TEXT_SIZE_LIMIT_IN_BYTES) {
// This may break markdown formatting
Logger.logVerbose(LOG_TAG, mReportInfo.reportTitle + " report string total size " + reportStringSize + " is greater than " + ACTIVITY_TEXT_SIZE_LIMIT_IN_BYTES + " and will be truncated");
mReportActivityMarkdownString = this.getString(R.string.msg_report_truncated) +
DataUtils.getTruncatedCommandOutput(reportString.toString(), ACTIVITY_TEXT_SIZE_LIMIT_IN_BYTES, true, false, false);
} else if (truncated) {
mReportActivityMarkdownString = this.getString(R.string.msg_report_truncated) + reportString.toString();
} else {
mReportActivityMarkdownString = reportString.toString();
}
} }
public static void startReportActivity(@NonNull final Context context, @NonNull final ReportInfo reportInfo) {
context.startActivity(newInstance(context, reportInfo));
public static class NewInstanceResult {
/** An intent that can be used to start the {@link ReportActivity}. */
public Intent contentIntent;
/** An intent that can should be adding as the {@link android.app.Notification#deleteIntent}
* by a call to {@link android.app.PendingIntent#getBroadcast(Context, int, Intent, int)}
* so that {@link ReportActivityBroadcastReceiver} can do cleanup of {@link #EXTRA_REPORT_INFO_OBJECT_FILE_PATH}. */
public Intent deleteIntent;
NewInstanceResult(Intent contentIntent, Intent deleteIntent) {
this.contentIntent = contentIntent;
this.deleteIntent = deleteIntent;
}
} }
public static Intent newInstance(@NonNull final Context context, @NonNull final ReportInfo reportInfo) { /**
* Start the {@link ReportActivity}.
*
* @param context The {@link Context} for operations.
* @param reportInfo The {@link ReportInfo} containing info that needs to be displayed.
*/
public static void startReportActivity(@NonNull final Context context, @NonNull ReportInfo reportInfo) {
NewInstanceResult result = newInstance(context, reportInfo);
if (result.contentIntent == null) return;
context.startActivity(result.contentIntent);
}
/**
* Get content and delete intents for the {@link ReportActivity} that can be used to start it
* and do cleanup.
*
* If {@link ReportInfo} size is too large, then a TransactionTooLargeException will be thrown
* so its object may be saved to a file in the {@link Context#getCacheDir()}. Then when activity
* starts, its read back and the file is deleted in {@link #onDestroy()}.
* Note that files may still be left if {@link #onDestroy()} is not called or doesn't finish.
* A separate cleanup routine is implemented from that case by
* {@link #deleteReportInfoFilesOlderThanXDays(Context, int, boolean)} which should be called
* incrementally or at app startup.
*
* @param context The {@link Context} for operations.
* @param reportInfo The {@link ReportInfo} containing info that needs to be displayed.
* @return Returns {@link NewInstanceResult}.
*/
@NonNull
public static NewInstanceResult newInstance(@NonNull final Context context, @NonNull final ReportInfo reportInfo) {
long size = DataUtils.getSerializedSize(reportInfo);
if (size > DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES) {
String reportInfoDirectoryPath = getReportInfoDirectoryPath(context);
String reportInfoFilePath = reportInfoDirectoryPath + "/" + CACHE_FILE_BASENAME_PREFIX + reportInfo.reportTimestamp;
Logger.logVerbose(LOG_TAG, reportInfo.reportTitle + " " + ReportInfo.class.getSimpleName() + " serialized object size " + size + " is greater than " + DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES + " and it will be written to file at path \"" + reportInfoFilePath + "\"");
Error error = FileUtils.writeSerializableObjectToFile(ReportInfo.class.getSimpleName(), reportInfoFilePath, reportInfo);
if (error != null) {
Logger.logErrorExtended(LOG_TAG, error.toString());
Logger.showToast(context, Error.getMinimalErrorString(error), true);
return new NewInstanceResult(null, null);
}
return new NewInstanceResult(createContentIntent(context, null, reportInfoFilePath),
createDeleteIntent(context, reportInfoFilePath));
} else {
return new NewInstanceResult(createContentIntent(context, reportInfo, null),
null);
}
}
private static Intent createContentIntent(@NonNull final Context context, final ReportInfo reportInfo, final String reportInfoFilePath) {
Intent intent = new Intent(context, ReportActivity.class); Intent intent = new Intent(context, ReportActivity.class);
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putSerializable(EXTRA_REPORT_INFO, reportInfo);
if (reportInfoFilePath != null) {
bundle.putString(EXTRA_REPORT_INFO_OBJECT_FILE_PATH, reportInfoFilePath);
} else {
bundle.putSerializable(EXTRA_REPORT_INFO_OBJECT, reportInfo);
}
intent.putExtras(bundle); intent.putExtras(bundle);
// Note that ReportActivity task has documentLaunchMode="intoExisting" set in AndroidManifest.xml // Note that ReportActivity should have `documentLaunchMode="intoExisting"` set in `AndroidManifest.xml`
// which has equivalent behaviour to the following. The following dynamic way doesn't seem to // which has equivalent behaviour to FLAG_ACTIVITY_NEW_DOCUMENT.
// work for notification pending intent, i.e separate task isn't created and activity is // FLAG_ACTIVITY_SINGLE_TOP must also be passed for onNewIntent to be called.
// launched in the same task as TermuxActivity. intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
return intent; return intent;
} }
private static Intent createDeleteIntent(@NonNull final Context context, final String reportInfoFilePath) {
if (reportInfoFilePath == null) return null;
Intent intent = new Intent(context, ReportActivityBroadcastReceiver.class);
intent.setAction(ACTION_DELETE_REPORT_INFO_OBJECT_FILE);
Bundle bundle = new Bundle();
bundle.putString(EXTRA_REPORT_INFO_OBJECT_FILE_PATH, reportInfoFilePath);
intent.putExtras(bundle);
return intent;
}
@NotNull
private static String getReportInfoDirectoryPath(Context context) {
// Canonicalize to solve /data/data and /data/user/0 issues when comparing with reportInfoFilePath
return FileUtils.getCanonicalPath(context.getCacheDir().getAbsolutePath(), null) + "/" + CACHE_DIR_BASENAME;
}
private static void deleteReportInfoFile(Context context, String reportInfoFilePath) {
if (context == null || reportInfoFilePath == null) return;
// Extra protection for mainly if someone set `exported="true"` for ReportActivityBroadcastReceiver
String reportInfoDirectoryPath = getReportInfoDirectoryPath(context);
reportInfoFilePath = FileUtils.getCanonicalPath(reportInfoFilePath, null);
if(!reportInfoFilePath.equals(reportInfoDirectoryPath) && reportInfoFilePath.startsWith(reportInfoDirectoryPath + "/")) {
Logger.logVerbose(LOG_TAG, "Deleting " + ReportInfo.class.getSimpleName() + " serialized object file at path \"" + reportInfoFilePath + "\"");
Error error = FileUtils.deleteRegularFile(ReportInfo.class.getSimpleName(), reportInfoFilePath, true);
if (error != null) {
Logger.logErrorExtended(LOG_TAG, error.toString());
}
} else {
Logger.logError(LOG_TAG, "Not deleting " + ReportInfo.class.getSimpleName() + " serialized object file at path \"" + reportInfoFilePath + "\" since its not under \"" + reportInfoDirectoryPath + "\"");
}
}
/**
* Delete {@link ReportInfo} serialized object files from cache older than x days. If a notification
* has still not been opened after x days that's using a PendingIntent to ReportActivity, then
* opening the notification will throw a file not found error, so choose days value appropriately
* or check if a notification is still active if tracking notification ids.
* The {@link Context} object passed must be of the same package with which {@link #newInstance(Context, ReportInfo)}
* was called since a call to {@link Context#getCacheDir()} is made.
*
* @param context The {@link Context} for operations.
* @param days The x amount of days before which files should be deleted. This must be `>=0`.
* @param isSynchronous If set to {@code true}, then the command will be executed in the
* caller thread and results returned synchronously.
* If set to {@code false}, then a new thread is started run the commands
* asynchronously in the background and control is returned to the caller thread.
* @return Returns the {@code error} if deleting was not successful, otherwise {@code null}.
*/
public static Error deleteReportInfoFilesOlderThanXDays(@NonNull final Context context, int days, final boolean isSynchronous) {
if (isSynchronous) {
return deleteReportInfoFilesOlderThanXDaysInner(context, days);
} else {
new Thread() { public void run() {
Error error = deleteReportInfoFilesOlderThanXDaysInner(context, days);
if (error != null) {
Logger.logErrorExtended(LOG_TAG, error.toString());
}
}}.start();
return null;
}
}
private static Error deleteReportInfoFilesOlderThanXDaysInner(@NonNull final Context context, int days) {
// Only regular files are deleted and subdirectories are not checked
String reportInfoDirectoryPath = getReportInfoDirectoryPath(context);
Logger.logVerbose(LOG_TAG, "Deleting " + ReportInfo.class.getSimpleName() + " serialized object files under directory path \"" + reportInfoDirectoryPath + "\" older than " + days + " days");
return FileUtils.deleteFilesOlderThanXDays(ReportInfo.class.getSimpleName(), reportInfoDirectoryPath, null, days, true, FileType.REGULAR.getValue());
}
/**
* The {@link BroadcastReceiver} for {@link ReportActivity} that currently does cleanup when
* {@link android.app.Notification#deleteIntent} is called. It must be registered in `AndroidManifest.xml`.
*/
public static class ReportActivityBroadcastReceiver extends BroadcastReceiver {
private static final String LOG_TAG = "ReportActivityBroadcastReceiver";
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null) return;
String action = intent.getAction();
Logger.logVerbose(LOG_TAG, "onReceive: \"" + action + "\" action");
if (ACTION_DELETE_REPORT_INFO_OBJECT_FILE.equals(action)) {
Bundle bundle = intent.getExtras();
if (bundle == null) return;
if (bundle.containsKey(EXTRA_REPORT_INFO_OBJECT_FILE_PATH)) {
deleteReportInfoFile(context, bundle.getString(EXTRA_REPORT_INFO_OBJECT_FILE_PATH));
}
}
}
}
} }

View File

@@ -0,0 +1,278 @@
package com.termux.shared.activities;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Typeface;
import android.os.Bundle;
import android.text.Editable;
import android.text.InputFilter;
import android.text.TextWatcher;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import com.termux.shared.interact.ShareUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.R;
import com.termux.shared.models.TextIOInfo;
import com.termux.shared.view.KeyboardUtils;
import org.jetbrains.annotations.NotNull;
import java.util.Locale;
/**
* An activity to edit or view text based on config passed as {@link TextIOInfo}.
*
* Add Following to `AndroidManifest.xml` to use in an app:
*
* {@code ` <activity android:name="com.termux.shared.activities.TextIOActivity" android:theme="@style/Theme.AppCompat.TermuxTextIOActivity" />` }
*/
public class TextIOActivity extends AppCompatActivity {
private static final String CLASS_NAME = ReportActivity.class.getCanonicalName();
public static final String EXTRA_TEXT_IO_INFO_OBJECT = CLASS_NAME + ".EXTRA_TEXT_IO_INFO_OBJECT";
private TextView mTextIOLabel;
private View mTextIOLabelSeparator;
private EditText mTextIOText;
private HorizontalScrollView mTextIOHorizontalScrollView;
private LinearLayout mTextIOTextLinearLayout;
private TextView mTextIOTextCharacterUsage;
private TextIOInfo mTextIOInfo;
private Bundle mBundle;
private static final String LOG_TAG = "TextIOActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Logger.logVerbose(LOG_TAG, "onCreate");
setContentView(R.layout.activity_text_io);
mTextIOLabel = findViewById(R.id.text_io_label);
mTextIOLabelSeparator = findViewById(R.id.text_io_label_separator);
mTextIOText = findViewById(R.id.text_io_text);
mTextIOHorizontalScrollView = findViewById(R.id.text_io_horizontal_scroll_view);
mTextIOTextLinearLayout = findViewById(R.id.text_io_text_linear_layout);
mTextIOTextCharacterUsage = findViewById(R.id.text_io_text_character_usage);
Toolbar toolbar = findViewById(R.id.toolbar);
if (toolbar != null) {
setSupportActionBar(toolbar);
}
mBundle = null;
Intent intent = getIntent();
if (intent != null)
mBundle = intent.getExtras();
else if (savedInstanceState != null)
mBundle = savedInstanceState;
updateUI();
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
Logger.logVerbose(LOG_TAG, "onNewIntent");
// Views must be re-created since different configs for isEditingTextDisabled() and
// isHorizontallyScrollable() will not work or at least reliably
finish();
startActivity(intent);
}
@SuppressLint("ClickableViewAccessibility")
private void updateUI() {
if (mBundle == null) {
finish(); return;
}
mTextIOInfo = (TextIOInfo) mBundle.getSerializable(EXTRA_TEXT_IO_INFO_OBJECT);
if (mTextIOInfo == null) {
finish(); return;
}
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
if (mTextIOInfo.getTitle() != null)
actionBar.setTitle(mTextIOInfo.getTitle());
else
actionBar.setTitle("Text Input");
if (mTextIOInfo.shouldShowBackButtonInActionBar()) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setDisplayShowHomeEnabled(true);
}
}
mTextIOLabel.setVisibility(View.GONE);
mTextIOLabelSeparator.setVisibility(View.GONE);
if (mTextIOInfo.isLabelEnabled()) {
mTextIOLabel.setVisibility(View.VISIBLE);
mTextIOLabelSeparator.setVisibility(View.VISIBLE);
mTextIOLabel.setText(mTextIOInfo.getLabel());
mTextIOLabel.setFilters(new InputFilter[] { new InputFilter.LengthFilter(TextIOInfo.LABEL_SIZE_LIMIT_IN_BYTES) });
mTextIOLabel.setTextSize(mTextIOInfo.getLabelSize());
mTextIOLabel.setTextColor(mTextIOInfo.getLabelColor());
mTextIOLabel.setTypeface(Typeface.create(mTextIOInfo.getLabelTypeFaceFamily(), mTextIOInfo.getLabelTypeFaceStyle()));
}
if (mTextIOInfo.isHorizontallyScrollable()) {
mTextIOHorizontalScrollView.setEnabled(true);
mTextIOText.setHorizontallyScrolling(true);
} else {
// Remove mTextIOHorizontalScrollView and add mTextIOText in its place
ViewGroup parent = (ViewGroup) mTextIOHorizontalScrollView.getParent();
if (parent != null && parent.indexOfChild(mTextIOText) < 0) {
ViewGroup.LayoutParams params = mTextIOHorizontalScrollView.getLayoutParams();
int index = parent.indexOfChild(mTextIOHorizontalScrollView);
mTextIOTextLinearLayout.removeAllViews();
mTextIOHorizontalScrollView.removeAllViews();
parent.removeView(mTextIOHorizontalScrollView);
parent.addView(mTextIOText, index, params);
mTextIOText.setHorizontallyScrolling(false);
}
}
mTextIOText.setText(mTextIOInfo.getText());
mTextIOText.setFilters(new InputFilter[] { new InputFilter.LengthFilter(mTextIOInfo.getTextLengthLimit()) });
mTextIOText.setTextSize(mTextIOInfo.getTextSize());
mTextIOText.setTextColor(mTextIOInfo.getTextColor());
mTextIOText.setTypeface(Typeface.create(mTextIOInfo.getTextTypeFaceFamily(), mTextIOInfo.getTextTypeFaceStyle()));
// setTextIsSelectable must be called after changing KeyListener to regain focusability and selectivity
if (mTextIOInfo.isEditingTextDisabled()) {
mTextIOText.setCursorVisible(false);
mTextIOText.setKeyListener(null);
mTextIOText.setTextIsSelectable(true);
}
if (mTextIOInfo.shouldShowTextCharacterUsage()) {
mTextIOTextCharacterUsage.setVisibility(View.VISIBLE);
updateTextIOTextCharacterUsage(mTextIOInfo.getText());
mTextIOText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable editable) {
if (editable != null)
updateTextIOTextCharacterUsage(editable.toString());
}
});
} else {
mTextIOTextCharacterUsage.setVisibility(View.GONE);
mTextIOText.addTextChangedListener(null);
}
}
private void updateTextIOInfoText() {
if (mTextIOText != null)
mTextIOInfo.setText(mTextIOText.getText().toString());
}
private void updateTextIOTextCharacterUsage(String text) {
if (text == null) text = "";
if (mTextIOTextCharacterUsage != null)
mTextIOTextCharacterUsage.setText(String.format(Locale.getDefault(), "%1$d/%2$d", text.length(), mTextIOInfo.getTextLengthLimit()));
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
updateTextIOInfoText();
outState.putSerializable(EXTRA_TEXT_IO_INFO_OBJECT, mTextIOInfo);
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
final MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_text_io, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
String text = "";
if (mTextIOText != null)
text = mTextIOText.getText().toString();
int id = item.getItemId();
if (id == android.R.id.home) {
confirm();
} if (id == R.id.menu_item_cancel) {
cancel();
} else if (id == R.id.menu_item_share_text) {
ShareUtils.shareText(this, mTextIOInfo.getTitle(), text);
} else if (id == R.id.menu_item_copy_text) {
ShareUtils.copyTextToClipboard(this, text, null);
}
return false;
}
@Override
public void onBackPressed() {
confirm();
}
/** Confirm current text and send it back to calling {@link Activity}. */
private void confirm() {
updateTextIOInfoText();
KeyboardUtils.hideSoftKeyboard(this, mTextIOText);
setResult(Activity.RESULT_OK, getResultIntent());
finish();
}
/** Cancel current text and notify calling {@link Activity}. */
private void cancel() {
KeyboardUtils.hideSoftKeyboard(this, mTextIOText);
setResult(Activity.RESULT_CANCELED, getResultIntent());
finish();
}
@NotNull
private Intent getResultIntent() {
Intent intent = new Intent();
Bundle bundle = new Bundle();
bundle.putSerializable(EXTRA_TEXT_IO_INFO_OBJECT, mTextIOInfo);
intent.putExtras(bundle);
return intent;
}
/**
* Get the {@link Intent} that can be used to start the {@link TextIOActivity}.
*
* @param context The {@link Context} for operations.
* @param textIOInfo The {@link TextIOInfo} containing info for the edit text.
*/
public static Intent newInstance(@NonNull final Context context, @NonNull final TextIOInfo textIOInfo) {
Intent intent = new Intent(context, TextIOActivity.class);
Bundle bundle = new Bundle();
bundle.putSerializable(EXTRA_TEXT_IO_INFO_OBJECT, textIOInfo);
intent.putExtras(bundle);
return intent;
}
}

View File

@@ -56,7 +56,7 @@ public class CrashHandler implements Thread.UncaughtExceptionHandler {
reportString.append("## Crash Details\n"); reportString.append("## Crash Details\n");
reportString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Crash Thread", thread.toString(), "-")); reportString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Crash Thread", thread.toString(), "-"));
reportString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Crash Timestamp", AndroidUtils.getCurrentTimeStamp(), "-")); reportString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Crash Timestamp", AndroidUtils.getCurrentMilliSecondUTCTimeStamp(), "-"));
reportString.append("\n\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Crash Message", throwable.getMessage(), "-")); reportString.append("\n\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Crash Message", throwable.getMessage(), "-"));
reportString.append("\n\n").append(Logger.getStackTracesMarkdownString("Stacktrace", Logger.getStackTracesStringArray(throwable))); reportString.append("\n\n").append(Logger.getStackTracesMarkdownString("Stacktrace", Logger.getStackTracesStringArray(throwable)));

View File

@@ -4,12 +4,14 @@ import android.os.Bundle;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.util.LinkedHashSet; import java.io.ByteArrayOutputStream;
import java.util.regex.Matcher; import java.io.ObjectOutputStream;
import java.util.regex.Pattern; import java.io.Serializable;
public class DataUtils { public class DataUtils {
/** Max safe limit of data size to prevent TransactionTooLargeException when transferring data
* inside or to other apps via transactions. */
public static final int TRANSACTION_SIZE_LIMIT_IN_BYTES = 100 * 1024; // 100KB public static final int TRANSACTION_SIZE_LIMIT_IN_BYTES = 100 * 1024; // 100KB
private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
@@ -97,6 +99,17 @@ public class DataUtils {
} }
} }
/**
* Get the {@code String} from an {@link Integer}.
*
* @param value The {@link Integer} value.
* @param def The default {@link String} value.
* @return Returns {@code value} if it is not {@code null}, otherwise returns {@code def}.
*/
public static String getStringFromInteger(Integer value, String def) {
return (value == null) ? def : String.valueOf((int) value);
}
/** /**
* Get the {@code hex string} from a {@link byte[]}. * Get the {@code hex string} from a {@link byte[]}.
* *
@@ -160,9 +173,37 @@ public class DataUtils {
return (object == null) ? def : object; return (object == null) ? def : object;
} }
/**
* Get the {@link String} itself if it is not {@code null} or empty, otherwise default.
*
* @param value The {@link String} to check.
* @param def The default {@link String}.
* @return Returns {@code value} if it is not {@code null} or empty, otherwise returns {@code def}.
*/
public static String getDefaultIfUnset(@Nullable String value, String def) {
return (value == null || value.isEmpty()) ? def : value;
}
/** Check if a string is null or empty. */ /** Check if a string is null or empty. */
public static boolean isNullOrEmpty(String string) { public static boolean isNullOrEmpty(String string) {
return string == null || string.isEmpty(); return string == null || string.isEmpty();
} }
/** Get size of a serializable object. */
public static long getSerializedSize(Serializable object) {
if (object == null) return 0;
try {
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteOutputStream);
objectOutputStream.writeObject(object);
objectOutputStream.flush();
objectOutputStream.close();
return byteOutputStream.toByteArray().length;
} catch (Exception e) {
return -1;
}
}
} }

View File

@@ -49,6 +49,29 @@ public class IntentUtils {
return value; return value;
} }
/**
* Get an {@link Integer} from an {@link Intent} stored as a {@link String} extra if its not
* {@code null} or empty.
*
* @param intent The {@link Intent} to get the extra from.
* @param key The {@link String} key name.
* @param def The default value if extra is not set.
* @return Returns the {@link Integer} extra if set, otherwise {@code null}.
*/
public static Integer getIntegerExtraIfSet(@NonNull Intent intent, String key, Integer def) {
try {
String value = intent.getStringExtra(key);
if (value == null || value.isEmpty()) {
return def;
}
return Integer.parseInt(value);
}
catch (Exception e) {
return def;
}
}
/** /**

View File

@@ -22,6 +22,7 @@ public class UrlUtils {
regex_sb.append("finger|"); // The Finger proto. regex_sb.append("finger|"); // The Finger proto.
regex_sb.append("ftp(?:s?)|"); // The FTP proto. regex_sb.append("ftp(?:s?)|"); // The FTP proto.
regex_sb.append("git|"); // The Git proto. regex_sb.append("git|"); // The Git proto.
regex_sb.append("gemini|"); // The Gemini proto.
regex_sb.append("gopher|"); // The Gopher proto. regex_sb.append("gopher|"); // The Gopher proto.
regex_sb.append("http(?:s?)|"); // The HTTP proto. regex_sb.append("http(?:s?)|"); // The HTTP proto.
regex_sb.append("imap(?:s?)|"); // The IMAP proto. regex_sb.append("imap(?:s?)|"); // The IMAP proto.

View File

@@ -10,10 +10,14 @@ import com.termux.shared.file.filesystem.FileType;
import com.termux.shared.file.filesystem.FileTypes; import com.termux.shared.file.filesystem.FileTypes;
import com.termux.shared.data.DataUtils; import com.termux.shared.data.DataUtils;
import com.termux.shared.logger.Logger; import com.termux.shared.logger.Logger;
import com.termux.shared.models.errors.Errno;
import com.termux.shared.models.errors.Error; import com.termux.shared.models.errors.Error;
import com.termux.shared.models.errors.FileUtilsErrno; import com.termux.shared.models.errors.FileUtilsErrno;
import com.termux.shared.models.errors.FunctionErrno; import com.termux.shared.models.errors.FunctionErrno;
import org.apache.commons.io.filefilter.AgeFileFilter;
import org.apache.commons.io.filefilter.IOFileFilter;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.BufferedWriter; import java.io.BufferedWriter;
import java.io.Closeable; import java.io.Closeable;
@@ -22,10 +26,17 @@ import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
import java.io.Serializable;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.file.LinkOption; import java.nio.file.LinkOption;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.util.Calendar;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Pattern; import java.util.regex.Pattern;
public class FileUtils { public class FileUtils {
@@ -100,6 +111,29 @@ public class FileUtils {
return path; return path;
} }
/**
* Convert special characters `\/:*?"<>|` to underscore.
*
* @param fileName The name to sanitize.
* @param sanitizeWhitespaces If set to {@code true}, then white space characters ` \t\n` will be
* converted.
* @param sanitizeWhitespaces If set to {@code true}, then file name will be converted to lowe case.
* @return Returns the {@code sanitized name}.
*/
public static String sanitizeFileName(String fileName, boolean sanitizeWhitespaces, boolean toLower) {
if (fileName == null) return null;
if (sanitizeWhitespaces)
fileName = fileName.replaceAll("[\\\\/:*?\"<>| \t\n]", "_");
else
fileName = fileName.replaceAll("[\\\\/:*?\"<>|]", "_");
if (toLower)
return fileName.toLowerCase();
else
return fileName;
}
/** /**
* Determines whether path is in {@code dirPath}. The {@code dirPath} is not canonicalized and * Determines whether path is in {@code dirPath}. The {@code dirPath} is not canonicalized and
* only normalized. * only normalized.
@@ -111,7 +145,21 @@ public class FileUtils {
* @return Returns {@code true} if path in {@code dirPath}, otherwise returns {@code false}. * @return Returns {@code true} if path in {@code dirPath}, otherwise returns {@code false}.
*/ */
public static boolean isPathInDirPath(String path, final String dirPath, final boolean ensureUnder) { public static boolean isPathInDirPath(String path, final String dirPath, final boolean ensureUnder) {
if (path == null || dirPath == null) return false; return isPathInDirPaths(path, Collections.singletonList(dirPath), ensureUnder);
}
/**
* Determines whether path is in one of the {@code dirPaths}. The {@code dirPaths} are not
* canonicalized and only normalized.
*
* @param path The {@code path} to check.
* @param dirPaths The {@code directory paths} to check in.
* @param ensureUnder If set to {@code true}, then it will be ensured that {@code path} is
* under the directories and does not equal it.
* @return Returns {@code true} if path in {@code dirPaths}, otherwise returns {@code false}.
*/
public static boolean isPathInDirPaths(String path, final List<String> dirPaths, final boolean ensureUnder) {
if (path == null || path.isEmpty() || dirPaths == null || dirPaths.size() < 1) return false;
try { try {
path = new File(path).getCanonicalPath(); path = new File(path).getCanonicalPath();
@@ -119,12 +167,20 @@ public class FileUtils {
return false; return false;
} }
String normalizedDirPath = normalizePath(dirPath); boolean isPathInDirPaths;
if (ensureUnder) for (String dirPath : dirPaths) {
return !path.equals(normalizedDirPath) && path.startsWith(normalizedDirPath + "/"); String normalizedDirPath = normalizePath(dirPath);
else
return path.startsWith(normalizedDirPath + "/"); if (ensureUnder)
isPathInDirPaths = !path.equals(normalizedDirPath) && path.startsWith(normalizedDirPath + "/");
else
isPathInDirPaths = path.startsWith(normalizedDirPath + "/");
if (isPathInDirPaths) return true;
}
return false;
} }
@@ -229,7 +285,7 @@ public class FileUtils {
// If file exists but not a regular file // If file exists but not a regular file
if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) { if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file"); return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file", filePath).setLabel(label + "file");
} }
boolean isPathUnderParentDirPath = false; boolean isPathUnderParentDirPath = false;
@@ -252,7 +308,8 @@ public class FileUtils {
// If path is not a regular file // If path is not a regular file
// Regular files cannot be automatically created so we do not ignore if missing // Regular files cannot be automatically created so we do not ignore if missing
if (fileType != FileType.REGULAR) { if (fileType != FileType.REGULAR) {
return FileUtilsErrno.ERRNO_NO_REGULAR_FILE_FOUND.getError(label + "file"); label += "regular file";
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, filePath).setLabel(label);
} }
// If there is not parentDirPath restriction or path is not under parentDirPath or // If there is not parentDirPath restriction or path is not under parentDirPath or
@@ -310,7 +367,7 @@ public class FileUtils {
// If file exists but not a directory file // If file exists but not a directory file
if (fileType != FileType.NO_EXIST && fileType != FileType.DIRECTORY) { if (fileType != FileType.NO_EXIST && fileType != FileType.DIRECTORY) {
return FileUtilsErrno.ERRNO_NON_DIRECTORY_FILE_FOUND.getError(label + "directory"); return FileUtilsErrno.ERRNO_NON_DIRECTORY_FILE_FOUND.getError(label + "directory", filePath).setLabel(label + "directory");
} }
boolean isPathInParentDirPath = false; boolean isPathInParentDirPath = false;
@@ -326,9 +383,10 @@ public class FileUtils {
if (createDirectoryIfMissing && fileType == FileType.NO_EXIST) { if (createDirectoryIfMissing && fileType == FileType.NO_EXIST) {
Logger.logVerbose(LOG_TAG, "Creating " + label + "directory file at path \"" + filePath + "\""); Logger.logVerbose(LOG_TAG, "Creating " + label + "directory file at path \"" + filePath + "\"");
// Create directory and update fileType if successful, otherwise return with error // Create directory and update fileType if successful, otherwise return with error
if (file.mkdirs()) // It "might" be possible that mkdirs returns false even though directory was created
fileType = getFileType(filePath, false); boolean result = file.mkdirs();
else fileType = getFileType(filePath, false);
if (!result && fileType != FileType.DIRECTORY)
return FileUtilsErrno.ERRNO_CREATING_FILE_FAILED.getError(label + "directory file", filePath); return FileUtilsErrno.ERRNO_CREATING_FILE_FAILED.getError(label + "directory file", filePath);
} }
@@ -348,7 +406,8 @@ public class FileUtils {
// If path is not a directory // If path is not a directory
// Directories can be automatically created so we can ignore if missing with above check // Directories can be automatically created so we can ignore if missing with above check
if (fileType != FileType.DIRECTORY) { if (fileType != FileType.DIRECTORY) {
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label + "directory", filePath); label += "directory";
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, filePath).setLabel(label);
} }
if (permissionsToCheck != null) { if (permissionsToCheck != null) {
@@ -423,7 +482,7 @@ public class FileUtils {
// If file exists but not a regular file // If file exists but not a regular file
if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) { if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file"); return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file", filePath).setLabel(label + "file");
} }
// If regular file already exists // If regular file already exists
@@ -613,8 +672,10 @@ public class FileUtils {
// If target file does not exist // If target file does not exist
if (targetFileType == FileType.NO_EXIST) { if (targetFileType == FileType.NO_EXIST) {
// If dangling symlink should not be allowed, then return with error // If dangling symlink should not be allowed, then return with error
if (!allowDangling) if (!allowDangling) {
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label + "symlink target file", targetFileAbsolutePath); label += "symlink target file";
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, targetFileAbsolutePath).setLabel(label);
}
} }
// If destination exists // If destination exists
@@ -888,8 +949,10 @@ public class FileUtils {
if (ignoreNonExistentSrcFile) if (ignoreNonExistentSrcFile)
return null; return null;
// Else return with error // Else return with error
else else {
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label + "source file", srcFilePath); label += "source file";
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, srcFilePath).setLabel(label);
}
} }
// If the file type of the source file does not exist in the allowedFileTypeFlags, then return with error // If the file type of the source file does not exist in the allowedFileTypeFlags, then return with error
@@ -995,7 +1058,7 @@ public class FileUtils {
/** /**
* Delete regular file at path. * Delete regular file at path.
* *
* This function is a wrapper for {@link #deleteFile(String, String, boolean, int)}. * This function is a wrapper for {@link #deleteFile(String, String, boolean, boolean, int)}.
* *
* @param label The optional label for file to delete. This can optionally be {@code null}. * @param label The optional label for file to delete. This can optionally be {@code null}.
* @param filePath The {@code path} for file to delete. * @param filePath The {@code path} for file to delete.
@@ -1004,13 +1067,13 @@ public class FileUtils {
* @return Returns the {@code error} if deletion was not successful, otherwise {@code null}. * @return Returns the {@code error} if deletion was not successful, otherwise {@code null}.
*/ */
public static Error deleteRegularFile(String label, final String filePath, final boolean ignoreNonExistentFile) { public static Error deleteRegularFile(String label, final String filePath, final boolean ignoreNonExistentFile) {
return deleteFile(label, filePath, ignoreNonExistentFile, FileType.REGULAR.getValue()); return deleteFile(label, filePath, ignoreNonExistentFile, false, FileType.REGULAR.getValue());
} }
/** /**
* Delete directory file at path. * Delete directory file at path.
* *
* This function is a wrapper for {@link #deleteFile(String, String, boolean, int)}. * This function is a wrapper for {@link #deleteFile(String, String, boolean, boolean, int)}.
* *
* @param label The optional label for file to delete. This can optionally be {@code null}. * @param label The optional label for file to delete. This can optionally be {@code null}.
* @param filePath The {@code path} for file to delete. * @param filePath The {@code path} for file to delete.
@@ -1019,13 +1082,13 @@ public class FileUtils {
* @return Returns the {@code error} if deletion was not successful, otherwise {@code null}. * @return Returns the {@code error} if deletion was not successful, otherwise {@code null}.
*/ */
public static Error deleteDirectoryFile(String label, final String filePath, final boolean ignoreNonExistentFile) { public static Error deleteDirectoryFile(String label, final String filePath, final boolean ignoreNonExistentFile) {
return deleteFile(label, filePath, ignoreNonExistentFile, FileType.DIRECTORY.getValue()); return deleteFile(label, filePath, ignoreNonExistentFile, false, FileType.DIRECTORY.getValue());
} }
/** /**
* Delete symlink file at path. * Delete symlink file at path.
* *
* This function is a wrapper for {@link #deleteFile(String, String, boolean, int)}. * This function is a wrapper for {@link #deleteFile(String, String, boolean, boolean, int)}.
* *
* @param label The optional label for file to delete. This can optionally be {@code null}. * @param label The optional label for file to delete. This can optionally be {@code null}.
* @param filePath The {@code path} for file to delete. * @param filePath The {@code path} for file to delete.
@@ -1034,13 +1097,13 @@ public class FileUtils {
* @return Returns the {@code error} if deletion was not successful, otherwise {@code null}. * @return Returns the {@code error} if deletion was not successful, otherwise {@code null}.
*/ */
public static Error deleteSymlinkFile(String label, final String filePath, final boolean ignoreNonExistentFile) { public static Error deleteSymlinkFile(String label, final String filePath, final boolean ignoreNonExistentFile) {
return deleteFile(label, filePath, ignoreNonExistentFile, FileType.SYMLINK.getValue()); return deleteFile(label, filePath, ignoreNonExistentFile, false, FileType.SYMLINK.getValue());
} }
/** /**
* Delete regular, directory or symlink file at path. * Delete regular, directory or symlink file at path.
* *
* This function is a wrapper for {@link #deleteFile(String, String, boolean, int)}. * This function is a wrapper for {@link #deleteFile(String, String, boolean, boolean, int)}.
* *
* @param label The optional label for file to delete. This can optionally be {@code null}. * @param label The optional label for file to delete. This can optionally be {@code null}.
* @param filePath The {@code path} for file to delete. * @param filePath The {@code path} for file to delete.
@@ -1049,7 +1112,7 @@ public class FileUtils {
* @return Returns the {@code error} if deletion was not successful, otherwise {@code null}. * @return Returns the {@code error} if deletion was not successful, otherwise {@code null}.
*/ */
public static Error deleteFile(String label, final String filePath, final boolean ignoreNonExistentFile) { public static Error deleteFile(String label, final String filePath, final boolean ignoreNonExistentFile) {
return deleteFile(label, filePath, ignoreNonExistentFile, FileTypes.FILE_TYPE_NORMAL_FLAGS); return deleteFile(label, filePath, ignoreNonExistentFile, false, FileTypes.FILE_TYPE_NORMAL_FLAGS);
} }
/** /**
@@ -1064,6 +1127,8 @@ public class FileUtils {
* @param filePath The {@code path} for file to delete. * @param filePath The {@code path} for file to delete.
* @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an * @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
* error if file to deleted doesn't exist. * error if file to deleted doesn't exist.
* @param ignoreWrongFileType The {@code boolean} that decides if it should be considered an
* error if file type is not one from {@code allowedFileTypeFlags}.
* @param allowedFileTypeFlags The flags that are matched against the file's {@link FileType} to * @param allowedFileTypeFlags The flags that are matched against the file's {@link FileType} to
* see if it should be deleted or not. This is a safety measure to * see if it should be deleted or not. This is a safety measure to
* prevent accidental deletion of the wrong type of file, like a * prevent accidental deletion of the wrong type of file, like a
@@ -1071,29 +1136,41 @@ public class FileUtils {
* {@link FileTypes#FILE_TYPE_ANY_FLAGS} to allow deletion of any file type. * {@link FileTypes#FILE_TYPE_ANY_FLAGS} to allow deletion of any file type.
* @return Returns the {@code error} if deletion was not successful, otherwise {@code null}. * @return Returns the {@code error} if deletion was not successful, otherwise {@code null}.
*/ */
public static Error deleteFile(String label, final String filePath, final boolean ignoreNonExistentFile, int allowedFileTypeFlags) { public static Error deleteFile(String label, final String filePath, final boolean ignoreNonExistentFile, final boolean ignoreWrongFileType, int allowedFileTypeFlags) {
label = (label == null ? "" : label + " "); label = (label == null ? "" : label + " ");
if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "deleteFile"); if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "deleteFile");
try { try {
Logger.logVerbose(LOG_TAG, "Deleting " + label + "file at path \"" + filePath + "\"");
File file = new File(filePath); File file = new File(filePath);
FileType fileType = getFileType(filePath, false); FileType fileType = getFileType(filePath, false);
Logger.logVerbose(LOG_TAG, "Processing delete of " + label + "file at path \"" + filePath + "\" of type \"" + fileType.getName() + "\"");
// If file does not exist // If file does not exist
if (fileType == FileType.NO_EXIST) { if (fileType == FileType.NO_EXIST) {
// If delete is to be ignored if file does not exist // If delete is to be ignored if file does not exist
if (ignoreNonExistentFile) if (ignoreNonExistentFile)
return null; return null;
// Else return with error // Else return with error
else else {
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label + "file meant to be deleted", filePath); label += "file meant to be deleted";
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, filePath).setLabel(label);
}
} }
// If the file type of the file does not exist in the allowedFileTypeFlags, then return with error // If the file type of the file does not exist in the allowedFileTypeFlags
if ((allowedFileTypeFlags & fileType.getValue()) <= 0) if ((allowedFileTypeFlags & fileType.getValue()) <= 0) {
// If wrong file type is to be ignored
if (ignoreWrongFileType) {
Logger.logVerbose(LOG_TAG, "Ignoring deletion of " + label + "file at path \"" + filePath + "\" not matching allowed file types: " + FileTypes.convertFileTypeFlagsToNamesString(allowedFileTypeFlags));
return null;
}
// Else return with error
return FileUtilsErrno.ERRNO_FILE_NOT_AN_ALLOWED_FILE_TYPE.getError(label + "file meant to be deleted", filePath, FileTypes.convertFileTypeFlagsToNamesString(allowedFileTypeFlags)); return FileUtilsErrno.ERRNO_FILE_NOT_AN_ALLOWED_FILE_TYPE.getError(label + "file meant to be deleted", filePath, FileTypes.convertFileTypeFlagsToNamesString(allowedFileTypeFlags));
}
Logger.logVerbose(LOG_TAG, "Deleting " + label + "file at path \"" + filePath + "\"");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
/* /*
@@ -1180,7 +1257,7 @@ public class FileUtils {
// If file exists but not a directory file // If file exists but not a directory file
if (fileType != FileType.NO_EXIST && fileType != FileType.DIRECTORY) { if (fileType != FileType.NO_EXIST && fileType != FileType.DIRECTORY) {
return FileUtilsErrno.ERRNO_NON_DIRECTORY_FILE_FOUND.getError(label + "directory"); return FileUtilsErrno.ERRNO_NON_DIRECTORY_FILE_FOUND.getError(label + "directory", filePath).setLabel(label + "directory");
} }
// If directory exists, clear its contents // If directory exists, clear its contents
@@ -1208,6 +1285,79 @@ public class FileUtils {
return null; return null;
} }
/**
* Delete files under a directory older than x days.
*
* The {@code filePath} must be the canonical path to a directory since symlinks will not be followed.
* Any symlink files found under the directory will be deleted, but not their targets.
*
* @param label The optional label for directory to clear. This can optionally be {@code null}.
* @param filePath The {@code path} for directory to clear.
* @param dirFilter The optional filter to apply when finding subdirectories.
* If this parameter is {@code null}, subdirectories will not be included in the
* search. Use TrueFileFilter.INSTANCE to match all directories.
* @param days The x amount of days before which files should be deleted. This must be `>=0`.
* @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
* error if file to deleted doesn't exist.
* @param allowedFileTypeFlags The flags that are matched against the file's {@link FileType} to
* see if it should be deleted or not. This is a safety measure to
* prevent accidental deletion of the wrong type of file, like a
* directory instead of a regular file. You can pass
* {@link FileTypes#FILE_TYPE_ANY_FLAGS} to allow deletion of any file type.
* @return Returns the {@code error} if deleting was not successful, otherwise {@code null}.
*/
public static Error deleteFilesOlderThanXDays(String label, final String filePath, final IOFileFilter dirFilter, int days, final boolean ignoreNonExistentFile, int allowedFileTypeFlags) {
label = (label == null ? "" : label + " ");
if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "deleteFilesOlderThanXDays");
if (days < 0) return FunctionErrno.ERRNO_INVALID_PARAMETER.getError(label + "days", "deleteFilesOlderThanXDays", " It must be >= 0.");
Error error;
try {
Logger.logVerbose(LOG_TAG, "Deleting files under " + label + "directory at path \"" + filePath + "\" older than " + days + " days");
File file = new File(filePath);
FileType fileType = getFileType(filePath, false);
// If file exists but not a directory file
if (fileType != FileType.NO_EXIST && fileType != FileType.DIRECTORY) {
return FileUtilsErrno.ERRNO_NON_DIRECTORY_FILE_FOUND.getError(label + "directory", filePath).setLabel(label + "directory");
}
// If file does not exist
if (fileType == FileType.NO_EXIST) {
// If delete is to be ignored if file does not exist
if (ignoreNonExistentFile)
return null;
// Else return with error
else {
label += "directory under which files had to be deleted";
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, filePath).setLabel(label);
}
}
// If directory exists, delete its contents
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DATE, -(days));
// AgeFileFilter seems to apply to symlink destination timestamp instead of symlink file itself
Iterator<File> filesToDelete =
org.apache.commons.io.FileUtils.iterateFiles(file, new AgeFileFilter(calendar.getTime()), dirFilter);
while (filesToDelete.hasNext()) {
File subFile = filesToDelete.next();
error = deleteFile(label + " directory sub", subFile.getAbsolutePath(), true, true, allowedFileTypeFlags);
if (error != null)
return error;
}
} catch (Exception e) {
return FileUtilsErrno.ERRNO_DELETING_FILES_OLDER_THAN_X_DAYS_FAILED_WITH_EXCEPTION.getError(e, label + "directory", filePath, days, e.getMessage());
}
return null;
}
/** /**
@@ -1234,7 +1384,7 @@ public class FileUtils {
// If file exists but not a regular file // If file exists but not a regular file
if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) { if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file"); return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file", filePath).setLabel(label + "file");
} }
// If file does not exist // If file does not exist
@@ -1243,8 +1393,10 @@ public class FileUtils {
if (ignoreNonExistentFile) if (ignoreNonExistentFile)
return null; return null;
// Else return with error // Else return with error
else else {
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label + "file meant to be read", filePath); label += "file meant to be read";
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, filePath).setLabel(label);
}
} }
if (charset == null) charset = Charset.defaultCharset(); if (charset == null) charset = Charset.defaultCharset();
@@ -1280,6 +1432,74 @@ public class FileUtils {
return null; return null;
} }
public static class ReadSerializableObjectResult {
public Error error;
public Serializable serializableObject;
ReadSerializableObjectResult(Error error, Serializable serializableObject) {
this.error = error;
this.serializableObject = serializableObject;
}
}
/**
* Read a {@link Serializable} object from file at path.
*
* @param label The optional label for file to read. This can optionally be {@code null}.
* @param filePath The {@code path} for file to read.
* @param readObjectType The {@link Class} of the object.
* @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
* error if file to read doesn't exist.
* @return Returns the {@code error} if reading was not successful, otherwise {@code null}.
*/
@NonNull
public static <T extends Serializable> ReadSerializableObjectResult readSerializableObjectFromFile(String label, final String filePath, Class<T> readObjectType, final boolean ignoreNonExistentFile) {
label = (label == null ? "" : label + " ");
if (filePath == null || filePath.isEmpty()) return new ReadSerializableObjectResult(FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "readSerializableObjectFromFile"), null);
Logger.logVerbose(LOG_TAG, "Reading serializable object from " + label + "file at path \"" + filePath + "\"");
T serializableObject;
FileType fileType = getFileType(filePath, false);
// If file exists but not a regular file
if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
return new ReadSerializableObjectResult(FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file", filePath).setLabel(label + "file"), null);
}
// If file does not exist
if (fileType == FileType.NO_EXIST) {
// If reading is to be ignored if file does not exist
if (ignoreNonExistentFile)
return new ReadSerializableObjectResult(null, null);
// Else return with error
else {
label += "file meant to be read";
return new ReadSerializableObjectResult(FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, filePath).setLabel(label), null);
}
}
FileInputStream fileInputStream = null;
ObjectInputStream objectInputStream = null;
try {
// Read string from file
fileInputStream = new FileInputStream(filePath);
objectInputStream = new ObjectInputStream(fileInputStream);
//serializableObject = (T) objectInputStream.readObject();
serializableObject = readObjectType.cast(objectInputStream.readObject());
//Logger.logVerbose(LOG_TAG, Logger.getMultiLineLogStringEntry("String", DataUtils.getTruncatedCommandOutput(dataStringBuilder.toString(), Logger.LOGGER_ENTRY_MAX_SAFE_PAYLOAD, true, false, true), "-"));
} catch (Exception e) {
return new ReadSerializableObjectResult(FileUtilsErrno.ERRNO_READING_SERIALIZABLE_OBJECT_TO_FILE_FAILED_WITH_EXCEPTION.getError(e, label + "file", filePath, e.getMessage()), null);
} finally {
closeCloseable(fileInputStream);
closeCloseable(objectInputStream);
}
return new ReadSerializableObjectResult(null, serializableObject);
}
/** /**
* Write the {@link String} {@code dataString} with a specific {@link Charset} to file at path. * Write the {@link String} {@code dataString} with a specific {@link Charset} to file at path.
* *
@@ -1287,6 +1507,7 @@ public class FileUtils {
* @param filePath The {@code path} for file to write. * @param filePath The {@code path} for file to write.
* @param charset The {@link Charset} of the {@code dataString}. If this is {@code null}, * @param charset The {@link Charset} of the {@code dataString}. If this is {@code null},
* then default {@link Charset} will be used. * then default {@link Charset} will be used.
* @param dataString The data to write to file.
* @param append The {@code boolean} that decides if file should be appended to or not. * @param append The {@code boolean} that decides if file should be appended to or not.
* @return Returns the {@code error} if writing was not successful, otherwise {@code null}. * @return Returns the {@code error} if writing was not successful, otherwise {@code null}.
*/ */
@@ -1298,15 +1519,7 @@ public class FileUtils {
Error error; Error error;
FileType fileType = getFileType(filePath, false); error = preWriteToFile(label, filePath);
// If file exists but not a regular file
if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file");
}
// Create the file parent directory
error = createParentDirectoryFile(label + "file parent", filePath);
if (error != null) if (error != null)
return error; return error;
@@ -1336,6 +1549,63 @@ public class FileUtils {
return null; return null;
} }
/**
* Write the {@link Serializable} {@code serializableObject} to file at path.
*
* @param label The optional label for file to write. This can optionally be {@code null}.
* @param filePath The {@code path} for file to write.
* @param serializableObject The object to write to file.
* @return Returns the {@code error} if writing was not successful, otherwise {@code null}.
*/
public static <T extends Serializable> Error writeSerializableObjectToFile(String label, final String filePath, final T serializableObject) {
label = (label == null ? "" : label + " ");
if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "writeSerializableObjectToFile");
Logger.logVerbose(LOG_TAG, "Writing serializable object to " + label + "file at path \"" + filePath + "\"");
Error error;
error = preWriteToFile(label, filePath);
if (error != null)
return error;
FileOutputStream fileOutputStream = null;
ObjectOutputStream objectOutputStream = null;
try {
// Write object to file
fileOutputStream = new FileOutputStream(filePath);
objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(serializableObject);
objectOutputStream.flush();
} catch (Exception e) {
return FileUtilsErrno.ERRNO_WRITING_SERIALIZABLE_OBJECT_TO_FILE_FAILED_WITH_EXCEPTION.getError(e, label + "file", filePath, e.getMessage());
} finally {
closeCloseable(fileOutputStream);
closeCloseable(objectOutputStream);
}
return null;
}
private static Error preWriteToFile(String label, String filePath) {
Error error;
FileType fileType = getFileType(filePath, false);
// If file exists but not a regular file
if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file", filePath).setLabel(label + "file");
}
// Create the file parent directory
error = createParentDirectoryFile(label + "file parent", filePath);
if (error != null)
return error;
return null;
}
/** /**
@@ -1533,17 +1803,17 @@ public class FileUtils {
// If file is not readable // If file is not readable
if (permissionsToCheck.contains("r") && !file.canRead()) { if (permissionsToCheck.contains("r") && !file.canRead()) {
return FileUtilsErrno.ERRNO_FILE_NOT_READABLE.getError(label + "file"); return FileUtilsErrno.ERRNO_FILE_NOT_READABLE.getError(label + "file", filePath).setLabel(label + "file");
} }
// If file is not writable // If file is not writable
if (permissionsToCheck.contains("w") && !file.canWrite()) { if (permissionsToCheck.contains("w") && !file.canWrite()) {
return FileUtilsErrno.ERRNO_FILE_NOT_WRITABLE.getError(label + "file"); return FileUtilsErrno.ERRNO_FILE_NOT_WRITABLE.getError(label + "file", filePath).setLabel(label + "file");
} }
// If file is not executable // If file is not executable
// This canExecute() will give "avc: granted { execute }" warnings for target sdk 29 // This canExecute() will give "avc: granted { execute }" warnings for target sdk 29
else if (permissionsToCheck.contains("x") && !file.canExecute() && !ignoreIfNotExecutable) { else if (permissionsToCheck.contains("x") && !file.canExecute() && !ignoreIfNotExecutable) {
return FileUtilsErrno.ERRNO_FILE_NOT_EXECUTABLE.getError(label + "file"); return FileUtilsErrno.ERRNO_FILE_NOT_EXECUTABLE.getError(label + "file", filePath).setLabel(label + "file");
} }
return null; return null;
@@ -1563,4 +1833,26 @@ public class FileUtils {
return Pattern.compile("^([r-])[w-][x-]$", 0).matcher(string).matches(); return Pattern.compile("^([r-])[w-][x-]$", 0).matcher(string).matches();
} }
/**
* Get a {@link Error} that contains a shorter version of {@link Errno} message.
*
* @param error The original {@link Error} returned by one of the {@link FileUtils} functions.
* @return Returns the shorter {@link Error} if one exists, otherwise original {@code error}.
*/
public static Error getShortFileUtilsError(final Error error) {
String type = error.getType();
if (!FileUtilsErrno.TYPE.equals(type)) return error;
Errno shortErrno = FileUtilsErrno.ERRNO_SHORT_MAPPING.get(Errno.valueOf(type, error.getCode()));
if (shortErrno == null) return error;
List<Throwable> throwables = error.getThrowablesList();
if (throwables.isEmpty())
return shortErrno.getError(DataUtils.getDefaultIfNull(error.getLabel(), "file"));
else
return shortErrno.getError(throwables, error.getLabel(), "file");
}
} }

View File

@@ -1,14 +1,47 @@
package com.termux.shared.file; package com.termux.shared.file;
import android.content.Context;
import android.os.Environment; import android.os.Environment;
import androidx.annotation.NonNull;
import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.models.ExecutionCommand;
import com.termux.shared.models.errors.Error; import com.termux.shared.models.errors.Error;
import com.termux.shared.models.errors.FileUtilsErrno;
import com.termux.shared.shell.TermuxShellEnvironmentClient;
import com.termux.shared.shell.TermuxTask;
import com.termux.shared.termux.AndroidUtils;
import com.termux.shared.termux.TermuxConstants; import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxUtils;
import java.io.File; import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern; import java.util.regex.Pattern;
public class TermuxFileUtils { public class TermuxFileUtils {
private static final String LOG_TAG = "TermuxFileUtils";
/**
* Replace "$PREFIX/" or "~/" prefix with termux absolute paths.
*
* @param paths The {@code paths} to expand.
* @return Returns the {@code expand paths}.
*/
public static List<String> getExpandedTermuxPaths(List<String> paths) {
if (paths == null) return null;
List<String> expandedPaths = new ArrayList<>();
for (int i = 0; i < paths.size(); i++) {
expandedPaths.add(getExpandedTermuxPath(paths.get(i)));
}
return expandedPaths;
}
/** /**
* Replace "$PREFIX/" or "~/" prefix with termux absolute paths. * Replace "$PREFIX/" or "~/" prefix with termux absolute paths.
* *
@@ -26,6 +59,23 @@ public class TermuxFileUtils {
return path; return path;
} }
/**
* Replace termux absolute paths with "$PREFIX/" or "~/" prefix.
*
* @param paths The {@code paths} to unexpand.
* @return Returns the {@code unexpand paths}.
*/
public static List<String> getUnExpandedTermuxPaths(List<String> paths) {
if (paths == null) return null;
List<String> unExpandedPaths = new ArrayList<>();
for (int i = 0; i < paths.size(); i++) {
unExpandedPaths.add(getUnExpandedTermuxPath(paths.get(i)));
}
return unExpandedPaths;
}
/** /**
* Replace termux absolute paths with "$PREFIX/" or "~/" prefix. * Replace termux absolute paths with "$PREFIX/" or "~/" prefix.
* *
@@ -120,4 +170,208 @@ public class TermuxFileUtils {
ignoreErrorsIfPathIsInParentDirPath, ignoreIfNotExecutable); ignoreErrorsIfPathIsInParentDirPath, ignoreIfNotExecutable);
} }
/**
* Validate if {@link TermuxConstants#TERMUX_FILES_DIR_PATH} exists and has
* {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS} permissions.
*
* This is required because binaries compiled for termux are hard coded with
* {@link TermuxConstants#TERMUX_PREFIX_DIR_PATH} and the path must be accessible.
*
* The permissions set to directory will be {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS}.
*
* This function does not create the directory manually but by calling {@link Context#getFilesDir()}
* so that android itself creates it. However, the call will not create its parent package
* data directory `/data/user/0/[package_name]` if it does not already exist and a `logcat`
* error will be logged by android.
* {@code Failed to ensure /data/user/0/<package_name>/files: mkdir failed: ENOENT (No such file or directory)}
* An android app normally can't create the package data directory since its parent `/data/user/0`
* is owned by `system` user and is normally created at app install or update time and not at app startup.
*
* Note that the path returned by {@link Context#getFilesDir()} may
* be under `/data/user/[id]/[package_name]` instead of `/data/data/[package_name]`
* defined by default by {@link TermuxConstants#TERMUX_FILES_DIR_PATH} where id will be 0 for
* primary user and a higher number for other users/profiles. If app is running under work profile
* or secondary user, then {@link TermuxConstants#TERMUX_FILES_DIR_PATH} will not be accessible
* and will not be automatically created, unless there is a bind mount from `/data/data` to
* `/data/user/[id]`, ideally in the right namespace.
* https://source.android.com/devices/tech/admin/multi-user
*
*
* On Android version `<=10`, the `/data/user/0` is a symlink to `/data/data` directory.
* https://cs.android.com/android/platform/superproject/+/android-10.0.0_r47:system/core/rootdir/init.rc;l=589
* {@code
* symlink /data/data /data/user/0
* }
*
* {@code
* /system/bin/ls -lhd /data/data /data/user/0
* drwxrwx--x 179 system system 8.0K 2021-xx-xx xx:xx /data/data
* lrwxrwxrwx 1 root root 10 2021-xx-xx xx:xx /data/user/0 -> /data/data
* }
*
* On Android version `>=11`, the `/data/data` directory is bind mounted at `/data/user/0`.
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:system/core/rootdir/init.rc;l=705
* https://cs.android.com/android/_/android/platform/system/core/+/3cca270e95ca8d8bc8b800e2b5d7da1825fd7100
* {@code
* # Unlink /data/user/0 if we previously symlink it to /data/data
* rm /data/user/0
*
* # Bind mount /data/user/0 to /data/data
* mkdir /data/user/0 0700 system system encryption=None
* mount none /data/data /data/user/0 bind rec
* }
*
* {@code
* /system/bin/grep -E '( /data )|( /data/data )|( /data/user/[0-9]+ )' /proc/self/mountinfo 2>&1 | /system/bin/grep -v '/data_mirror' 2>&1
* 87 32 253:5 / /data rw,nosuid,nodev,noatime shared:27 - ext4 /dev/block/dm-5 rw,seclabel,resgid=1065,errors=panic
* 91 87 253:5 /data /data/user/0 rw,nosuid,nodev,noatime shared:27 - ext4 /dev/block/dm-5 rw,seclabel,resgid=1065,errors=panic
* }
*
* The column 4 defines the root of the mount within the filesystem.
* Basically, `/dev/block/dm-5/` is mounted at `/data` and `/dev/block/dm-5/data` is mounted at
* `/data/user/0`.
* https://www.kernel.org/doc/Documentation/filesystems/proc.txt (section 3.5)
* https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt
* https://unix.stackexchange.com/a/571959
*
*
* Also note that running `/system/bin/ls -lhd /data/user/0/com.termux` as secondary user will result
* in `ls: /data/user/0/com.termux: Permission denied` where `0` is primary user id but running
* `/system/bin/ls -lhd /data/user/10/com.termux` will result in
* `drwx------ 6 u10_a149 u10_a149 4.0K 2021-xx-xx xx:xx /data/user/10/com.termux` where `10` is
* secondary user id. So can't stat directory (not contents) of primary user from secondary user
* but can the other way around. However, this is happening on android 10 avd, but not on android
* 11 avd.
*
* @param context The {@link Context} for operations.
* @param createDirectoryIfMissing The {@code boolean} that decides if directory file
* should be created if its missing.
* @param setMissingPermissions The {@code boolean} that decides if permissions are to be
* automatically set.
* @return Returns the {@code error} if path is not a directory file, failed to create it,
* or validating permissions failed, otherwise {@code null}.
*/
public static Error isTermuxFilesDirectoryAccessible(@NonNull final Context context, boolean createDirectoryIfMissing, boolean setMissingPermissions) {
if (createDirectoryIfMissing)
context.getFilesDir();
if (!FileUtils.directoryFileExists(TermuxConstants.TERMUX_FILES_DIR_PATH, true))
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError("termux files directory", TermuxConstants.TERMUX_FILES_DIR_PATH);
if (setMissingPermissions)
FileUtils.setMissingFilePermissions("termux files directory", TermuxConstants.TERMUX_FILES_DIR_PATH,
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS);
return FileUtils.checkMissingFilePermissions("termux files directory", TermuxConstants.TERMUX_FILES_DIR_PATH,
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS, false);
}
/**
* Validate if {@link TermuxConstants#TERMUX_PREFIX_DIR_PATH} exists and has
* {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS} permissions.
* .
*
* The {@link TermuxConstants#TERMUX_PREFIX_DIR_PATH} directory would not exist if termux has
* not been installed or the bootstrap setup has not been run or if it was deleted by the user.
*
* @param createDirectoryIfMissing The {@code boolean} that decides if directory file
* should be created if its missing.
* @param setMissingPermissions The {@code boolean} that decides if permissions are to be
* automatically set.
* @return Returns the {@code error} if path is not a directory file, failed to create it,
* or validating permissions failed, otherwise {@code null}.
*/
public static Error isTermuxPrefixDirectoryAccessible(boolean createDirectoryIfMissing, boolean setMissingPermissions) {
return FileUtils.validateDirectoryFileExistenceAndPermissions("termux prefix directory", TermuxConstants.TERMUX_PREFIX_DIR_PATH,
null, createDirectoryIfMissing,
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS, setMissingPermissions, true,
false, false);
}
/**
* Validate if {@link TermuxConstants#TERMUX_STAGING_PREFIX_DIR_PATH} exists and has
* {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS} permissions.
*
* @param createDirectoryIfMissing The {@code boolean} that decides if directory file
* should be created if its missing.
* @param setMissingPermissions The {@code boolean} that decides if permissions are to be
* automatically set.
* @return Returns the {@code error} if path is not a directory file, failed to create it,
* or validating permissions failed, otherwise {@code null}.
*/
public static Error isTermuxPrefixStagingDirectoryAccessible(boolean createDirectoryIfMissing, boolean setMissingPermissions) {
return FileUtils.validateDirectoryFileExistenceAndPermissions("termux prefix staging directory", TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH,
null, createDirectoryIfMissing,
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS, setMissingPermissions, true,
false, false);
}
/**
* Get a markdown {@link String} for stat output for various Termux app files paths.
*
* @param context The context for operations.
* @return Returns the markdown {@link String}.
*/
public static String getTermuxFilesStatMarkdownString(@NonNull final Context context) {
Context termuxPackageContext = TermuxUtils.getTermuxPackageContext(context);
if (termuxPackageContext == null) return null;
// Also ensures that termux files directory is created if it does not already exist
String filesDir = termuxPackageContext.getFilesDir().getAbsolutePath();
// Build script
StringBuilder statScript = new StringBuilder();
statScript
.append("echo 'ls info:'\n")
.append("/system/bin/ls -lhdZ")
.append(" '/data/data'")
.append(" '/data/user/0'")
.append(" '" + TermuxConstants.TERMUX_INTERNAL_PRIVATE_APP_DATA_DIR_PATH + "'")
.append(" '/data/user/0/" + TermuxConstants.TERMUX_PACKAGE_NAME + "'")
.append(" '" + TermuxConstants.TERMUX_FILES_DIR_PATH + "'")
.append(" '" + filesDir + "'")
.append(" '/data/user/0/" + TermuxConstants.TERMUX_PACKAGE_NAME + "/files'")
.append(" '/data/user/" + TermuxConstants.TERMUX_PACKAGE_NAME + "/files'")
.append(" '" + TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH + "'")
.append(" '" + TermuxConstants.TERMUX_PREFIX_DIR_PATH + "'")
.append(" '" + TermuxConstants.TERMUX_HOME_DIR_PATH + "'")
.append(" '" + TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/login'")
.append(" 2>&1")
.append("\necho; echo 'mount info:'\n")
.append("/system/bin/grep -E '( /data )|( /data/data )|( /data/user/[0-9]+ )' /proc/self/mountinfo 2>&1 | /system/bin/grep -v '/data_mirror' 2>&1");
// Run script
ExecutionCommand executionCommand = new ExecutionCommand(1, "/system/bin/sh", null, statScript.toString() + "\n", "/", true, true);
executionCommand.commandLabel = TermuxConstants.TERMUX_APP_NAME + " Files Stat Command";
executionCommand.backgroundCustomLogLevel = Logger.LOG_LEVEL_OFF;
TermuxTask termuxTask = TermuxTask.execute(context, executionCommand, null, new TermuxShellEnvironmentClient(), true);
if (termuxTask == null || !executionCommand.isSuccessful()) {
Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
return null;
}
// Build script output
StringBuilder statOutput = new StringBuilder();
statOutput.append("$ ").append(statScript.toString());
statOutput.append("\n\n").append(executionCommand.resultData.stdout.toString());
boolean stderrSet = !executionCommand.resultData.stderr.toString().isEmpty();
if (executionCommand.resultData.exitCode != 0 || stderrSet) {
Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
if (stderrSet)
statOutput.append("\n").append(executionCommand.resultData.stderr.toString());
statOutput.append("\n").append("exit code: ").append(executionCommand.resultData.exitCode.toString());
}
// Build markdown output
StringBuilder markdownString = new StringBuilder();
markdownString.append("## ").append(TermuxConstants.TERMUX_APP_NAME).append(" Files Info\n\n");
AndroidUtils.appendPropertyToMarkdown(markdownString,"TERMUX_REQUIRED_FILES_DIR_PATH ($PREFIX)", TermuxConstants.TERMUX_FILES_DIR_PATH);
AndroidUtils.appendPropertyToMarkdown(markdownString,"ANDROID_ASSIGNED_FILES_DIR_PATH", filesDir);
markdownString.append("\n\n").append(MarkdownUtils.getMarkdownCodeForString(statOutput.toString(), true));
markdownString.append("\n##\n");
return markdownString.toString();
}
} }

View File

@@ -4,11 +4,14 @@ import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.graphics.Color;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.widget.Button;
import android.widget.TextView; import android.widget.TextView;
import com.termux.shared.R; import com.termux.shared.R;
import com.termux.shared.logger.Logger;
public class MessageDialogUtils { public class MessageDialogUtils {
@@ -22,9 +25,33 @@ public class MessageDialogUtils {
* @param onDismiss The {@link DialogInterface.OnDismissListener} to run when dialog is dismissed. * @param onDismiss The {@link DialogInterface.OnDismissListener} to run when dialog is dismissed.
*/ */
public static void showMessage(Context context, String titleText, String messageText, final DialogInterface.OnDismissListener onDismiss) { public static void showMessage(Context context, String titleText, String messageText, final DialogInterface.OnDismissListener onDismiss) {
showMessage(context, titleText, messageText, null, null, null, null, onDismiss);
}
AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.Theme_AppCompat_Light_Dialog) /**
.setPositiveButton(android.R.string.ok, null); * Show a message in a dialog
*
* @param context The {@link Context} to use to start the dialog. An {@link Activity} {@link Context}
* must be passed, otherwise exceptions will be thrown.
* @param titleText The title text of the dialog.
* @param messageText The message text of the dialog.
* @param positiveText The positive button text of the dialog.
* @param onPositiveButton The {@link DialogInterface.OnClickListener} to run when positive button
* is pressed.
* @param negativeText The negative button text of the dialog. If this is {@code null}, then
* negative button will not be shown.
* @param onNegativeButton The {@link DialogInterface.OnClickListener} to run when negative button
* is pressed.
* @param onDismiss The {@link DialogInterface.OnDismissListener} to run when dialog is dismissed.
*/
public static void showMessage(Context context, String titleText, String messageText,
String positiveText,
final DialogInterface.OnClickListener onPositiveButton,
String negativeText,
final DialogInterface.OnClickListener onNegativeButton,
final DialogInterface.OnDismissListener onDismiss) {
AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.Theme_AppCompat_Light_Dialog);
LayoutInflater inflater = (LayoutInflater) context.getSystemService( Context.LAYOUT_INFLATER_SERVICE ); LayoutInflater inflater = (LayoutInflater) context.getSystemService( Context.LAYOUT_INFLATER_SERVICE );
View view = inflater.inflate(R.layout.dialog_show_message, null); View view = inflater.inflate(R.layout.dialog_show_message, null);
@@ -40,10 +67,29 @@ public class MessageDialogUtils {
messageView.setText(messageText); messageView.setText(messageText);
} }
if (positiveText == null)
positiveText = context.getString(android.R.string.ok);
builder.setPositiveButton(positiveText, onPositiveButton);
if (negativeText != null)
builder.setNegativeButton(negativeText, onNegativeButton);
if (onDismiss != null) if (onDismiss != null)
builder.setOnDismissListener(onDismiss); builder.setOnDismissListener(onDismiss);
builder.show(); AlertDialog dialog = builder.create();
dialog.setOnShowListener(dialogInterface -> {
Logger.logError("dialog");
Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
if (button != null)
button.setTextColor(Color.BLACK);
button = dialog.getButton(AlertDialog.BUTTON_NEGATIVE);
if (button != null)
button.setTextColor(Color.BLACK);
});
dialog.show();
} }
public static void exitAppWithErrorMessage(Context context, String titleText, String messageText) { public static void exitAppWithErrorMessage(Context context, String titleText, String messageText) {

View File

@@ -1,16 +1,27 @@
package com.termux.shared.interact; package com.termux.shared.interact;
import android.Manifest;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.ClipData; import android.content.ClipData;
import android.content.ClipboardManager; import android.content.ClipboardManager;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Environment;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import com.termux.shared.R; import com.termux.shared.R;
import com.termux.shared.data.DataUtils; import com.termux.shared.data.DataUtils;
import com.termux.shared.data.IntentUtils;
import com.termux.shared.file.FileUtils;
import com.termux.shared.logger.Logger; import com.termux.shared.logger.Logger;
import com.termux.shared.models.errors.Error;
import com.termux.shared.packages.PermissionUtils;
import java.nio.charset.Charset;
public class ShareUtils { public class ShareUtils {
@@ -30,7 +41,11 @@ public class ShareUtils {
chooserIntent.putExtra(Intent.EXTRA_INTENT, intent); chooserIntent.putExtra(Intent.EXTRA_INTENT, intent);
chooserIntent.putExtra(Intent.EXTRA_TITLE, title); chooserIntent.putExtra(Intent.EXTRA_TITLE, title);
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(chooserIntent); try {
context.startActivity(chooserIntent);
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to open system chooser for:\n" + IntentUtils.getIntentString(chooserIntent), e);
}
} }
/** /**
@@ -41,12 +56,12 @@ public class ShareUtils {
* @param text The text to share. * @param text The text to share.
*/ */
public static void shareText(final Context context, final String subject, final String text) { public static void shareText(final Context context, final String subject, final String text) {
if (context == null) return; if (context == null || text == null) return;
final Intent shareTextIntent = new Intent(Intent.ACTION_SEND); final Intent shareTextIntent = new Intent(Intent.ACTION_SEND);
shareTextIntent.setType("text/plain"); shareTextIntent.setType("text/plain");
shareTextIntent.putExtra(Intent.EXTRA_SUBJECT, subject); shareTextIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
shareTextIntent.putExtra(Intent.EXTRA_TEXT, DataUtils.getTruncatedCommandOutput(text, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false)); 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, context.getString(R.string.title_share_with));
} }
@@ -60,12 +75,12 @@ public class ShareUtils {
* clipboard is successful. * clipboard is successful.
*/ */
public static void copyTextToClipboard(final Context context, final String text, final String toastString) { public static void copyTextToClipboard(final Context context, final String text, final String toastString) {
if (context == null) return; if (context == null || text == null) return;
final ClipboardManager clipboardManager = ContextCompat.getSystemService(context, ClipboardManager.class); final ClipboardManager clipboardManager = ContextCompat.getSystemService(context, ClipboardManager.class);
if (clipboardManager != null) { if (clipboardManager != null) {
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, text)); clipboardManager.setPrimaryClip(ClipData.newPlainText(null, DataUtils.getTruncatedCommandOutput(text, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, true, false, false)));
if (toastString != null && !toastString.isEmpty()) if (toastString != null && !toastString.isEmpty())
Logger.showToast(context, toastString, true); Logger.showToast(context, toastString, true);
} }
@@ -79,12 +94,61 @@ public class ShareUtils {
*/ */
public static void openURL(final Context context, final String url) { public static void openURL(final Context context, final String url) {
if (context == null || url == null || url.isEmpty()) return; if (context == null || url == null || url.isEmpty()) return;
Uri uri = Uri.parse(url);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
try { try {
Uri uri = Uri.parse(url);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
context.startActivity(intent); context.startActivity(intent);
} catch (ActivityNotFoundException e) {
// If no activity found to handle intent, show system chooser
openSystemAppChooser(context, intent, context.getString(R.string.title_open_url_with));
} catch (Exception e) { } catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to open the url \"" + url + "\"", e); Logger.logStackTraceWithMessage(LOG_TAG, "Failed to open url \"" + url + "\"", e);
}
}
/**
* Save a file at the path.
*
* If if path is under {@link Environment#getExternalStorageDirectory()}
* or `/sdcard` and storage permission is missing, it will be requested if {@code context} is an
* instance of {@link Activity} or {@link AppCompatActivity} and {@code storagePermissionRequestCode}
* is `>=0` and the function will automatically return. The caller should call this function again
* if user granted the permission.
*
* @param context The context for operations.
* @param label The label for file.
* @param filePath The path to save the file.
* @param text The text to write to file.
* @param showToast If set to {@code true}, then a toast is shown if saving to file is successful.
* @param storagePermissionRequestCode The request code to use while asking for permission.
*/
public static void saveTextToFile(final Context context, final String label, final String filePath, final String text, final boolean showToast, final int storagePermissionRequestCode) {
if (context == null || filePath == null || filePath.isEmpty() || text == null) return;
// If path is under primary external storage directory, then check for missing permissions.
if ((FileUtils.isPathInDirPath(filePath, Environment.getExternalStorageDirectory().getAbsolutePath(), true) ||
FileUtils.isPathInDirPath(filePath, "/sdcard", true)) &&
!PermissionUtils.checkPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
Logger.logErrorAndShowToast(context, LOG_TAG, context.getString(R.string.msg_storage_permission_not_granted));
if (storagePermissionRequestCode >= 0) {
if (context instanceof AppCompatActivity)
PermissionUtils.requestPermission(((AppCompatActivity) context), Manifest.permission.WRITE_EXTERNAL_STORAGE, storagePermissionRequestCode);
else if (context instanceof Activity)
PermissionUtils.requestPermission(((Activity) context), Manifest.permission.WRITE_EXTERNAL_STORAGE, storagePermissionRequestCode);
}
return;
}
Error error = FileUtils.writeStringToFile(label, filePath,
Charset.defaultCharset(), text, false);
if (error != null) {
Logger.logErrorExtended(LOG_TAG, error.toString());
Logger.showToast(context, Error.getMinimalErrorString(error), true);
} else {
if (showToast)
Logger.showToast(context, context.getString(R.string.msg_file_saved_successfully, label, filePath), true);
} }
} }

View File

@@ -7,6 +7,7 @@ import android.util.Log;
import android.widget.Toast; import android.widget.Toast;
import com.termux.shared.R; import com.termux.shared.R;
import com.termux.shared.data.DataUtils;
import com.termux.shared.termux.TermuxConstants; import com.termux.shared.termux.TermuxConstants;
import java.io.IOException; import java.io.IOException;
@@ -26,6 +27,7 @@ public class Logger {
public static final int LOG_LEVEL_VERBOSE = 3; // start logging verbose messages public static final int LOG_LEVEL_VERBOSE = 3; // start logging verbose messages
public static final int DEFAULT_LOG_LEVEL = LOG_LEVEL_NORMAL; public static final int DEFAULT_LOG_LEVEL = LOG_LEVEL_NORMAL;
public static final int MAX_LOG_LEVEL = LOG_LEVEL_VERBOSE;
private static int CURRENT_LOG_LEVEL = DEFAULT_LOG_LEVEL; private static int CURRENT_LOG_LEVEL = DEFAULT_LOG_LEVEL;
/** /**
@@ -50,16 +52,16 @@ public class Logger {
public static void logMessage(int logLevel, String tag, String message) { public static void logMessage(int logPriority, String tag, String message) {
if (logLevel == Log.ERROR && CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL) if (logPriority == Log.ERROR && CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL)
Log.e(getFullTag(tag), message); Log.e(getFullTag(tag), message);
else if (logLevel == Log.WARN && CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL) else if (logPriority == Log.WARN && CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL)
Log.w(getFullTag(tag), message); Log.w(getFullTag(tag), message);
else if (logLevel == Log.INFO && CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL) else if (logPriority == Log.INFO && CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL)
Log.i(getFullTag(tag), message); Log.i(getFullTag(tag), message);
else if (logLevel == Log.DEBUG && CURRENT_LOG_LEVEL >= LOG_LEVEL_DEBUG) else if (logPriority == Log.DEBUG && CURRENT_LOG_LEVEL >= LOG_LEVEL_DEBUG)
Log.d(getFullTag(tag), message); Log.d(getFullTag(tag), message);
else if (logLevel == Log.VERBOSE && CURRENT_LOG_LEVEL >= LOG_LEVEL_VERBOSE) else if (logPriority == Log.VERBOSE && CURRENT_LOG_LEVEL >= LOG_LEVEL_VERBOSE)
Log.v(getFullTag(tag), message); Log.v(getFullTag(tag), message);
} }
@@ -187,6 +189,10 @@ public class Logger {
logExtendedMessage(Log.VERBOSE, DEFAULT_LOG_TAG, message); logExtendedMessage(Log.VERBOSE, DEFAULT_LOG_TAG, message);
} }
public static void logVerboseForce(String tag, String message) {
Log.v(tag, message);
}
public static void logErrorAndShowToast(Context context, String tag, String message) { public static void logErrorAndShowToast(Context context, String tag, String message) {
@@ -358,7 +364,7 @@ public class Logger {
public static void showToast(final Context context, final String toastText, boolean longDuration) { public static void showToast(final Context context, final String toastText, boolean longDuration) {
if (context == null) return; if (context == null || DataUtils.isNullOrEmpty(toastText)) return;
new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(context, toastText, longDuration ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show()); new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(context, toastText, longDuration ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show());
} }
@@ -409,7 +415,7 @@ public class Logger {
} }
public static int setLogLevel(Context context, int logLevel) { public static int setLogLevel(Context context, int logLevel) {
if (logLevel >= LOG_LEVEL_OFF && logLevel <= LOG_LEVEL_VERBOSE) if (isLogLevelValid(logLevel))
CURRENT_LOG_LEVEL = logLevel; CURRENT_LOG_LEVEL = logLevel;
else else
CURRENT_LOG_LEVEL = DEFAULT_LOG_LEVEL; CURRENT_LOG_LEVEL = DEFAULT_LOG_LEVEL;
@@ -427,4 +433,16 @@ public class Logger {
return DEFAULT_LOG_TAG + ":" + tag; return DEFAULT_LOG_TAG + ":" + tag;
} }
public static boolean isLogLevelValid(Integer logLevel) {
return (logLevel != null && logLevel >= LOG_LEVEL_OFF && logLevel <= MAX_LOG_LEVEL);
}
/** Check if custom log level is valid and >= {@link #CURRENT_LOG_LEVEL}. If custom log level is
* not valid then {@link #LOG_LEVEL_VERBOSE} must be >= {@link #CURRENT_LOG_LEVEL}. */
public static boolean shouldEnableLoggingForCustomLogLevel(Integer customLogLevel) {
if (customLogLevel == null || CURRENT_LOG_LEVEL <= LOG_LEVEL_OFF || customLogLevel <= LOG_LEVEL_OFF) return false;
customLogLevel = Logger.isLogLevelValid(customLogLevel) ? customLogLevel: Logger.LOG_LEVEL_VERBOSE;
return (customLogLevel >= CURRENT_LOG_LEVEL);
}
} }

View File

@@ -119,9 +119,9 @@ public class MarkdownUtils {
return "**" + label + "**: " + def + "\n"; return "**" + label + "**: " + def + "\n";
} }
public static String getLinkMarkdownString(String label, Object object) { public static String getLinkMarkdownString(String label, String url) {
if (object != null) if (url != null)
return "[" + label + "](" + object + ")"; return "[" + label.replaceAll("]", "\\\\]") + "](" + url.replaceAll("\\)", "\\\\)") + ")";
else else
return label; return label;
} }
@@ -191,9 +191,8 @@ public class MarkdownUtils {
} }
public static Spanned getSpannedMarkdownText(Context context, String string) { public static Spanned getSpannedMarkdownText(Context context, String string) {
if (context == null || string == null) return null;
final Markwon markwon = getSpannedMarkwonBuilder(context); final Markwon markwon = getSpannedMarkwonBuilder(context);
return markwon.toMarkdown(string); return markwon.toMarkdown(string);
} }

View File

@@ -83,6 +83,14 @@ public class ExecutionCommand {
/** If the {@link ExecutionCommand} is meant to start a failsafe terminal session. */ /** If the {@link ExecutionCommand} is meant to start a failsafe terminal session. */
public boolean isFailsafe; public boolean isFailsafe;
/**
* The {@link ExecutionCommand} custom log level for background {@link com.termux.shared.shell.TermuxTask}
* commands. By default, @link com.termux.shared.shell.StreamGobbler} only logs stdout and
* stderr if {@link Logger} `CURRENT_LOG_LEVEL` is >= {@link Logger#LOG_LEVEL_VERBOSE} and
* {@link com.termux.shared.shell.TermuxTask} only logs stdin if `CURRENT_LOG_LEVEL` is >=
* {@link Logger#LOG_LEVEL_DEBUG}.
*/
public Integer backgroundCustomLogLevel;
/** The session action of foreground commands. */ /** The session action of foreground commands. */
public String sessionAction; public String sessionAction;
@@ -234,9 +242,9 @@ public class ExecutionCommand {
@Override @Override
public String toString() { public String toString() {
if (!hasExecuted()) if (!hasExecuted())
return getExecutionInputLogString(this, true); return getExecutionInputLogString(this, true, true);
else { else {
return getExecutionOutputLogString(this, true, true); return getExecutionOutputLogString(this, true, true, true);
} }
} }
@@ -245,9 +253,10 @@ public class ExecutionCommand {
* *
* @param executionCommand The {@link ExecutionCommand} to convert. * @param executionCommand The {@link ExecutionCommand} to convert.
* @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored. * @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored.
* @param logStdin Set to {@code true} if {@link #stdin} should be logged.
* @return Returns the log friendly {@link String}. * @return Returns the log friendly {@link String}.
*/ */
public static String getExecutionInputLogString(final ExecutionCommand executionCommand, boolean ignoreNull) { public static String getExecutionInputLogString(final ExecutionCommand executionCommand, boolean ignoreNull, boolean logStdin) {
if (executionCommand == null) return "null"; if (executionCommand == null) return "null";
StringBuilder logString = new StringBuilder(); StringBuilder logString = new StringBuilder();
@@ -264,6 +273,14 @@ public class ExecutionCommand {
logString.append("\n").append(executionCommand.getInBackgroundLogString()); logString.append("\n").append(executionCommand.getInBackgroundLogString());
logString.append("\n").append(executionCommand.getIsFailsafeLogString()); logString.append("\n").append(executionCommand.getIsFailsafeLogString());
if (executionCommand.inBackground) {
if (logStdin && (!ignoreNull || !DataUtils.isNullOrEmpty(executionCommand.stdin)))
logString.append("\n").append(executionCommand.getStdinLogString());
if (!ignoreNull || executionCommand.backgroundCustomLogLevel != null)
logString.append("\n").append(executionCommand.getBackgroundCustomLogLevelLogString());
}
if (!ignoreNull || executionCommand.sessionAction != null) if (!ignoreNull || executionCommand.sessionAction != null)
logString.append("\n").append(executionCommand.getSessionActionLogString()); logString.append("\n").append(executionCommand.getSessionActionLogString());
@@ -283,9 +300,10 @@ public class ExecutionCommand {
* @param executionCommand The {@link ExecutionCommand} to convert. * @param executionCommand The {@link ExecutionCommand} to convert.
* @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored. * @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored.
* @param logResultData Set to {@code true} if {@link #resultData} should be logged. * @param logResultData Set to {@code true} if {@link #resultData} should be logged.
* @param logStdoutAndStderr Set to {@code true} if {@link ResultData#stdout} and {@link ResultData#stderr} should be logged.
* @return Returns the log friendly {@link String}. * @return Returns the log friendly {@link String}.
*/ */
public static String getExecutionOutputLogString(final ExecutionCommand executionCommand, boolean ignoreNull, boolean logResultData) { public static String getExecutionOutputLogString(final ExecutionCommand executionCommand, boolean ignoreNull, boolean logResultData, boolean logStdoutAndStderr) {
if (executionCommand == null) return "null"; if (executionCommand == null) return "null";
StringBuilder logString = new StringBuilder(); StringBuilder logString = new StringBuilder();
@@ -296,7 +314,7 @@ public class ExecutionCommand {
logString.append("\n").append(executionCommand.getCurrentStateLogString()); logString.append("\n").append(executionCommand.getCurrentStateLogString());
if (logResultData) if (logResultData)
logString.append("\n").append(ResultData.getResultDataLogString(executionCommand.resultData, ignoreNull)); logString.append("\n").append(ResultData.getResultDataLogString(executionCommand.resultData, logStdoutAndStderr));
return logString.toString(); return logString.toString();
} }
@@ -312,8 +330,8 @@ public class ExecutionCommand {
StringBuilder logString = new StringBuilder(); StringBuilder logString = new StringBuilder();
logString.append(getExecutionInputLogString(executionCommand, false)); logString.append(getExecutionInputLogString(executionCommand, false, true));
logString.append(getExecutionOutputLogString(executionCommand, false, true)); logString.append(getExecutionOutputLogString(executionCommand, false, true, true));
logString.append("\n").append(executionCommand.getCommandDescriptionLogString()); logString.append("\n").append(executionCommand.getCommandDescriptionLogString());
logString.append("\n").append(executionCommand.getCommandHelpLogString()); logString.append("\n").append(executionCommand.getCommandHelpLogString());
@@ -346,6 +364,14 @@ public class ExecutionCommand {
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Working Directory", executionCommand.workingDirectory, "-")); markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Working Directory", executionCommand.workingDirectory, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("inBackground", executionCommand.inBackground, "-")); markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("inBackground", executionCommand.inBackground, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("isFailsafe", executionCommand.isFailsafe, "-")); markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("isFailsafe", executionCommand.isFailsafe, "-"));
if (executionCommand.inBackground) {
if (!DataUtils.isNullOrEmpty(executionCommand.stdin))
markdownString.append("\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Stdin", executionCommand.stdin, "-"));
if (executionCommand.backgroundCustomLogLevel != null)
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Background Custom Log Level", executionCommand.backgroundCustomLogLevel, "-"));
}
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Session Action", executionCommand.sessionAction, "-")); markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Session Action", executionCommand.sessionAction, "-"));
@@ -418,6 +444,17 @@ public class ExecutionCommand {
return "isFailsafe: `" + isFailsafe + "`"; return "isFailsafe: `" + isFailsafe + "`";
} }
public String getStdinLogString() {
if (DataUtils.isNullOrEmpty(stdin))
return "Stdin: -";
else
return Logger.getMultiLineLogStringEntry("Stdin", stdin, "-");
}
public String getBackgroundCustomLogLevelLogString() {
return "Background Custom Log Level: `" + backgroundCustomLogLevel + "`";
}
public String getSessionActionLogString() { public String getSessionActionLogString() {
return Logger.getSingleLineLogStringEntry("Session Action", sessionAction, "-"); return Logger.getSingleLineLogStringEntry("Session Action", sessionAction, "-");
} }

View File

@@ -5,6 +5,9 @@ import com.termux.shared.termux.AndroidUtils;
import java.io.Serializable; import java.io.Serializable;
/**
* An object that stored info for {@link com.termux.shared.activities.ReportActivity}.
*/
public class ReportInfo implements Serializable { public class ReportInfo implements Serializable {
/** The user action that was being processed for which the report was generated. */ /** The user action that was being processed for which the report was generated. */
@@ -14,27 +17,36 @@ public class ReportInfo implements Serializable {
/** The report title. */ /** The report title. */
public final String reportTitle; public final String reportTitle;
/** The markdown report text prefix. Will not be part of copy and share operations, etc. */ /** The markdown report text prefix. Will not be part of copy and share operations, etc. */
public final String reportStringPrefix; public String reportStringPrefix;
/** The markdown report text. */ /** The markdown report text. */
public final String reportString; public String reportString;
/** The markdown report text suffix. Will not be part of copy and share operations, etc. */ /** The markdown report text suffix. Will not be part of copy and share operations, etc. */
public final String reportStringSuffix; public String reportStringSuffix;
/** If set to {@code true}, then report, app and device info will be added to the report when /** If set to {@code true}, then report, app and device info will be added to the report when
* markdown is generated. * markdown is generated.
*/ */
public final boolean addReportInfoToMarkdown; public final boolean addReportInfoHeaderToMarkdown;
/** The timestamp for the report. */ /** The timestamp for the report. */
public final String reportTimestamp; public final String reportTimestamp;
public ReportInfo(String userAction, String sender, String reportTitle, String reportStringPrefix, String reportString, String reportStringSuffix, boolean addReportInfoToMarkdown) { /** The label for the report file to save if user selects menu_item_save_report_to_file. */
public final String reportSaveFileLabel;
/** The path for the report file to save if user selects menu_item_save_report_to_file. */
public final String reportSaveFilePath;
public ReportInfo(String userAction, String sender, String reportTitle, String reportStringPrefix,
String reportString, String reportStringSuffix, boolean addReportInfoHeaderToMarkdown,
String reportSaveFileLabel, String reportSaveFilePath) {
this.userAction = userAction; this.userAction = userAction;
this.sender = sender; this.sender = sender;
this.reportTitle = reportTitle; this.reportTitle = reportTitle;
this.reportStringPrefix = reportStringPrefix; this.reportStringPrefix = reportStringPrefix;
this.reportString = reportString; this.reportString = reportString;
this.reportStringSuffix = reportStringSuffix; this.reportStringSuffix = reportStringSuffix;
this.addReportInfoToMarkdown = addReportInfoToMarkdown; this.addReportInfoHeaderToMarkdown = addReportInfoHeaderToMarkdown;
this.reportTimestamp = AndroidUtils.getCurrentTimeStamp(); this.reportSaveFileLabel = reportSaveFileLabel;
this.reportSaveFilePath = reportSaveFilePath;
this.reportTimestamp = AndroidUtils.getCurrentMilliSecondUTCTimeStamp();
} }
/** /**
@@ -48,7 +60,7 @@ public class ReportInfo implements Serializable {
StringBuilder markdownString = new StringBuilder(); StringBuilder markdownString = new StringBuilder();
if (reportInfo.addReportInfoToMarkdown) { if (reportInfo.addReportInfoHeaderToMarkdown) {
markdownString.append("## Report Info\n\n"); markdownString.append("## Report Info\n\n");
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("User Action", reportInfo.userAction, "-")); markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("User Action", reportInfo.userAction, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Sender", reportInfo.sender, "-")); markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Sender", reportInfo.sender, "-"));

View File

@@ -133,16 +133,18 @@ public class ResultData implements Serializable {
* Get a log friendly {@link String} for {@link ResultData} parameters. * Get a log friendly {@link String} for {@link ResultData} parameters.
* *
* @param resultData The {@link ResultData} to convert. * @param resultData The {@link ResultData} to convert.
* @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored. * @param logStdoutAndStderr Set to {@code true} if {@link #stdout} and {@link #stderr} should be logged.
* @return Returns the log friendly {@link String}. * @return Returns the log friendly {@link String}.
*/ */
public static String getResultDataLogString(final ResultData resultData, boolean ignoreNull) { public static String getResultDataLogString(final ResultData resultData, boolean logStdoutAndStderr) {
if (resultData == null) return "null"; if (resultData == null) return "null";
StringBuilder logString = new StringBuilder(); StringBuilder logString = new StringBuilder();
logString.append("\n").append(resultData.getStdoutLogString()); if (logStdoutAndStderr) {
logString.append("\n").append(resultData.getStderrLogString()); logString.append("\n").append(resultData.getStdoutLogString());
logString.append("\n").append(resultData.getStderrLogString());
}
logString.append("\n").append(resultData.getExitCodeLogString()); logString.append("\n").append(resultData.getExitCodeLogString());
logString.append("\n\n").append(getErrorsListLogString(resultData)); logString.append("\n\n").append(getErrorsListLogString(resultData));

View File

@@ -0,0 +1,234 @@
package com.termux.shared.models;
import android.graphics.Color;
import android.graphics.Typeface;
import androidx.annotation.NonNull;
import com.termux.shared.activities.TextIOActivity;
import com.termux.shared.data.DataUtils;
import java.io.Serializable;
/**
* An object that stored info for {@link TextIOActivity}.
* Max text limit is 95KB to prevent TransactionTooLargeException as per
* {@link DataUtils#TRANSACTION_SIZE_LIMIT_IN_BYTES}. Larger size can be supported for in-app
* transactions by storing {@link TextIOInfo} as a serialized object in a file like
* {@link com.termux.shared.activities.ReportActivity} does.
*/
public class TextIOInfo implements Serializable {
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
/** The action for which {@link TextIOActivity} will be started. */
private final String mAction;
/** The internal app component that is will start the {@link TextIOActivity}. */
private final String mSender;
/** The activity title. */
private String mTitle;
/** If back button should be shown in {@link android.app.ActionBar}. */
private boolean mShowBackButtonInActionBar = false;
/** If label is enabled. */
private boolean mLabelEnabled = false;
/**
* The label of text input set in {@link android.widget.TextView} that can be updated by user.
* Max allowed length is {@link #LABEL_SIZE_LIMIT_IN_BYTES}.
*/
private String mLabel;
/** The text size of label. Defaults to 14sp. */
private int mLabelSize = 14;
/** The text color of label. Defaults to {@link Color#BLACK}. */
private int mLabelColor = Color.BLACK;
/** The {@link Typeface} family of label. Defaults to "sans-serif". */
private String mLabelTypeFaceFamily = "sans-serif";
/** The {@link Typeface} style of label. Defaults to {@link Typeface#BOLD}. */
private int mLabelTypeFaceStyle = Typeface.BOLD;
/**
* The text of text input set in {@link android.widget.EditText} that can be updated by user.
* Max allowed length is {@link #TEXT_SIZE_LIMIT_IN_BYTES}.
*/
private String mText;
/** The text size for text. Defaults to 12sp. */
private int mTextSize = 12;
/** The text size for text. Defaults to {@link #TEXT_SIZE_LIMIT_IN_BYTES}. */
private int mTextLengthLimit = TEXT_SIZE_LIMIT_IN_BYTES;
/** The text color of text. Defaults to {@link Color#BLACK}. */
private int mTextColor = Color.BLACK;
/** The {@link Typeface} family for text. Defaults to "sans-serif". */
private String mTextTypeFaceFamily = "sans-serif";
/** The {@link Typeface} style for text. Defaults to {@link Typeface#NORMAL}. */
private int mTextTypeFaceStyle = Typeface.NORMAL;
/** If horizontal scrolling should be enabled for text. */
private boolean mTextHorizontallyScrolling = false;
/** If character usage should be enabled for text. */
private boolean mShowTextCharacterUsage = false;
/** If editing text should be disabled so that text acts like its in a {@link android.widget.TextView}. */
private boolean mEditingTextDisabled = false;
public TextIOInfo(@NonNull String action, @NonNull String sender) {
mAction = action;
mSender = sender;
}
public String getAction() {
return mAction;
}
public String getSender() {
return mSender;
}
public String getTitle() {
return mTitle;
}
public void setTitle(String title) {
mTitle = title;
}
public boolean shouldShowBackButtonInActionBar() {
return mShowBackButtonInActionBar;
}
public void setShowBackButtonInActionBar(boolean showBackButtonInActionBar) {
mShowBackButtonInActionBar = showBackButtonInActionBar;
}
public boolean isLabelEnabled() {
return mLabelEnabled;
}
public void setLabelEnabled(boolean labelEnabled) {
mLabelEnabled = labelEnabled;
}
public String getLabel() {
return mLabel;
}
public void setLabel(String label) {
mLabel = DataUtils.getTruncatedCommandOutput(label, LABEL_SIZE_LIMIT_IN_BYTES, true, false, false);
}
public int getLabelSize() {
return mLabelSize;
}
public void setLabelSize(int labelSize) {
if (labelSize > 0)
mLabelSize = labelSize;
}
public int getLabelColor() {
return mLabelColor;
}
public void setLabelColor(int labelColor) {
mLabelColor = labelColor;
}
public String getLabelTypeFaceFamily() {
return mLabelTypeFaceFamily;
}
public void setLabelTypeFaceFamily(String labelTypeFaceFamily) {
mLabelTypeFaceFamily = labelTypeFaceFamily;
}
public int getLabelTypeFaceStyle() {
return mLabelTypeFaceStyle;
}
public void setLabelTypeFaceStyle(int labelTypeFaceStyle) {
mLabelTypeFaceStyle = labelTypeFaceStyle;
}
public String getText() {
return mText;
}
public void setText(String text) {
mText = DataUtils.getTruncatedCommandOutput(text, TEXT_SIZE_LIMIT_IN_BYTES, true, false, false);
}
public int getTextSize() {
return mTextSize;
}
public void setTextSize(int textSize) {
if (textSize > 0)
mTextSize = textSize;
}
public int getTextLengthLimit() {
return mTextLengthLimit;
}
public void setTextLengthLimit(int textLengthLimit) {
if (textLengthLimit < TEXT_SIZE_LIMIT_IN_BYTES)
mTextLengthLimit = textLengthLimit;
}
public int getTextColor() {
return mTextColor;
}
public void setTextColor(int textColor) {
mTextColor = textColor;
}
public String getTextTypeFaceFamily() {
return mTextTypeFaceFamily;
}
public void setTextTypeFaceFamily(String textTypeFaceFamily) {
mTextTypeFaceFamily = textTypeFaceFamily;
}
public int getTextTypeFaceStyle() {
return mTextTypeFaceStyle;
}
public void setTextTypeFaceStyle(int textTypeFaceStyle) {
mTextTypeFaceStyle = textTypeFaceStyle;
}
public boolean isHorizontallyScrollable() {
return mTextHorizontallyScrolling;
}
public void setTextHorizontallyScrolling(boolean textHorizontallyScrolling) {
mTextHorizontallyScrolling = textHorizontallyScrolling;
}
public boolean shouldShowTextCharacterUsage() {
return mShowTextCharacterUsage;
}
public void setShowTextCharacterUsage(boolean showTextCharacterUsage) {
mShowTextCharacterUsage = showTextCharacterUsage;
}
public boolean isEditingTextDisabled() {
return mEditingTextDisabled;
}
public void setEditingTextDisabled(boolean editingTextDisabled) {
mEditingTextDisabled = editingTextDisabled;
}
}

View File

@@ -8,11 +8,14 @@ import com.termux.shared.logger.Logger;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
/** The {@link Class} that defines error messages and codes. */ /** The {@link Class} that defines error messages and codes. */
public class Errno { public class Errno {
private static final HashMap<String, Errno> map = new HashMap<>();
public static final String TYPE = "Error"; public static final String TYPE = "Error";
@@ -35,6 +38,7 @@ public class Errno {
this.type = type; this.type = type;
this.code = code; this.code = code;
this.message = message; this.message = message;
map.put(type + ":" + code, this);
} }
@NonNull @NonNull
@@ -56,6 +60,17 @@ public class Errno {
return code; return code;
} }
/**
* Get the {@link Errno} of a specific type and code.
*
* @param type The unique type of the {@link Errno}.
* @param code The unique code of the {@link Errno}.
*/
public static Errno valueOf(String type, Integer code) {
if (type == null || type.isEmpty() || code == null) return null;
return map.get(type + ":" + code);
}
public Error getError() { public Error getError() {
@@ -73,12 +88,18 @@ public class Errno {
} }
public Error getError(Throwable throwable, Object... args) { public Error getError(Throwable throwable, Object... args) {
return getError(Collections.singletonList(throwable), args); if (throwable == null)
return getError(args);
else
return getError(Collections.singletonList(throwable), args);
} }
public Error getError(List<Throwable> throwablesList, Object... args) { public Error getError(List<Throwable> throwablesList, Object... args) {
try { try {
return new Error(getType(), getCode(), String.format(getMessage(), args), throwablesList); if (throwablesList == null)
return new Error(getType(), getCode(), String.format(getMessage(), args));
else
return new Error(getType(), getCode(), String.format(getMessage(), args), throwablesList);
} catch (Exception e) { } catch (Exception e) {
Logger.logWarn(LOG_TAG, "Exception raised while calling String.format() for error message of errno " + this + " with args" + Arrays.toString(args) + "\n" + e.getMessage()); Logger.logWarn(LOG_TAG, "Exception raised while calling String.format() for error message of errno " + this + " with args" + Arrays.toString(args) + "\n" + e.getMessage());
// Return unformatted message as a backup // Return unformatted message as a backup

View File

@@ -12,6 +12,8 @@ import java.util.List;
public class Error implements Serializable { public class Error implements Serializable {
/** The optional error label. */
private String label;
/** The error type. */ /** The error type. */
private String type; private String type;
/** The error code. */ /** The error code. */
@@ -76,7 +78,18 @@ public class Error implements Serializable {
this.code = Errno.ERRNO_SUCCESS.getCode(); this.code = Errno.ERRNO_SUCCESS.getCode();
this.message = message; this.message = message;
this.throwablesList = throwablesList;
if (throwablesList != null)
this.throwablesList = throwablesList;
}
public Error setLabel(String label) {
this.label = label;
return this;
}
public String getLabel() {
return label;
} }

View File

@@ -1,5 +1,8 @@
package com.termux.shared.models.errors; package com.termux.shared.models.errors;
import java.util.HashMap;
import java.util.Map;
/** The {@link Class} that defines FileUtils error messages and codes. */ /** The {@link Class} that defines FileUtils error messages and codes. */
public class FileUtilsErrno extends Errno { public class FileUtilsErrno extends Errno {
@@ -18,19 +21,20 @@ public class FileUtilsErrno extends Errno {
/* Errors for invalid or not found files at path (150-200) */ /* Errors for invalid or not found files at path (150-200) */
public static final Errno ERRNO_FILE_NOT_FOUND_AT_PATH = new Errno(TYPE, 150, "The %1$s is not found at path \"%2$s\"."); public static final Errno ERRNO_FILE_NOT_FOUND_AT_PATH = new Errno(TYPE, 150, "The %1$s not found at path \"%2$s\".");
public static final Errno ERRNO_FILE_NOT_FOUND_AT_PATH_SHORT = new Errno(TYPE, 151, "The %1$s not found at path.");
public static final Errno ERRNO_NO_REGULAR_FILE_FOUND = new Errno(TYPE, 151, "Regular file not found at %1$s path."); public static final Errno ERRNO_NON_REGULAR_FILE_FOUND = new Errno(TYPE, 152, "Non-regular file found at %1$s path \"%2$s\".");
public static final Errno ERRNO_NOT_A_REGULAR_FILE = new Errno(TYPE, 152, "The %1$s at path \"%2$s\" is not a regular file."); public static final Errno ERRNO_NON_REGULAR_FILE_FOUND_SHORT = new Errno(TYPE, 153, "Non-regular file found at %1$s path.");
public static final Errno ERRNO_NON_DIRECTORY_FILE_FOUND = new Errno(TYPE, 154, "Non-directory file found at %1$s path \"%2$s\".");
public static final Errno ERRNO_NON_DIRECTORY_FILE_FOUND_SHORT = new Errno(TYPE, 155, "Non-directory file found at %1$s path.");
public static final Errno ERRNO_NON_SYMLINK_FILE_FOUND = new Errno(TYPE, 156, "Non-symlink file found at %1$s path \"%2$s\".");
public static final Errno ERRNO_NON_SYMLINK_FILE_FOUND_SHORT = new Errno(TYPE, 157, "Non-symlink file found at %1$s path.");
public static final Errno ERRNO_NON_REGULAR_FILE_FOUND = new Errno(TYPE, 153, "Non-regular file found at %1$s path."); public static final Errno ERRNO_FILE_NOT_AN_ALLOWED_FILE_TYPE = new Errno(TYPE, 158, "The %1$s found at path \"%2$s\" is not one of allowed file types \"%3$s\".");
public static final Errno ERRNO_NON_DIRECTORY_FILE_FOUND = new Errno(TYPE, 154, "Non-directory file found at %1$s path.");
public static final Errno ERRNO_NON_SYMLINK_FILE_FOUND = new Errno(TYPE, 155, "Non-symlink file found at %1$s path.");
public static final Errno ERRNO_FILE_NOT_AN_ALLOWED_FILE_TYPE = new Errno(TYPE, 156, "The %1$s found at path \"%2$s\" is not one of allowed file types \"%3$s\"."); public static final Errno ERRNO_VALIDATE_FILE_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 159, "Validating file existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s");
public static final Errno ERRNO_VALIDATE_DIRECTORY_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 160, "Validating directory existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s");
public static final Errno ERRNO_VALIDATE_FILE_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 157, "Validating file existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s");
public static final Errno ERRNO_VALIDATE_DIRECTORY_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 158, "Validating directory existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s");
@@ -56,6 +60,7 @@ public class FileUtilsErrno extends Errno {
public static final Errno ERRNO_DELETING_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 301, "Deleting %1$s at path \"%2$s\" failed.\nException: %3$s"); public static final Errno ERRNO_DELETING_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 301, "Deleting %1$s at path \"%2$s\" failed.\nException: %3$s");
public static final Errno ERRNO_CLEARING_DIRECTORY_FAILED_WITH_EXCEPTION = new Errno(TYPE, 302, "Clearing %1$s at path \"%2$s\" failed.\nException: %3$s"); public static final Errno ERRNO_CLEARING_DIRECTORY_FAILED_WITH_EXCEPTION = new Errno(TYPE, 302, "Clearing %1$s at path \"%2$s\" failed.\nException: %3$s");
public static final Errno ERRNO_FILE_STILL_EXISTS_AFTER_DELETING = new Errno(TYPE, 303, "The %1$s still exists after deleting it from \"%2$s\"."); public static final Errno ERRNO_FILE_STILL_EXISTS_AFTER_DELETING = new Errno(TYPE, 303, "The %1$s still exists after deleting it from \"%2$s\".");
public static final Errno ERRNO_DELETING_FILES_OLDER_THAN_X_DAYS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 304, "Deleting %1$s under directory at path \"%2$s\" old than %3$s days failed.\nException: %4$s");
@@ -64,18 +69,38 @@ public class FileUtilsErrno extends Errno {
public static final Errno ERRNO_WRITING_STRING_TO_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 351, "Writing string to %1$s at path \"%2$s\" failed.\nException: %3$s"); public static final Errno ERRNO_WRITING_STRING_TO_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 351, "Writing string to %1$s at path \"%2$s\" failed.\nException: %3$s");
public static final Errno ERRNO_UNSUPPORTED_CHARSET = new Errno(TYPE, 352, "Unsupported charset \"%1$s\""); public static final Errno ERRNO_UNSUPPORTED_CHARSET = new Errno(TYPE, 352, "Unsupported charset \"%1$s\"");
public static final Errno ERRNO_CHECKING_IF_CHARSET_SUPPORTED_FAILED = new Errno(TYPE, 353, "Checking if charset \"%1$s\" is supported failed.\nException: %2$s"); public static final Errno ERRNO_CHECKING_IF_CHARSET_SUPPORTED_FAILED = new Errno(TYPE, 353, "Checking if charset \"%1$s\" is supported failed.\nException: %2$s");
public static final Errno ERRNO_READING_SERIALIZABLE_OBJECT_TO_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 354, "Reading serializable object from %1$s at path \"%2$s\" failed.\nException: %3$s");
public static final Errno ERRNO_WRITING_SERIALIZABLE_OBJECT_TO_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 355, "Writing serializable object to %1$s at path \"%2$s\" failed.\nException: %3$s");
/* Errors for invalid file permissions (400-450) */ /* Errors for invalid file permissions (400-450) */
public static final Errno ERRNO_INVALID_FILE_PERMISSIONS_STRING_TO_CHECK = new Errno(TYPE, 400, "The file permission string to check is invalid."); public static final Errno ERRNO_INVALID_FILE_PERMISSIONS_STRING_TO_CHECK = new Errno(TYPE, 400, "The file permission string to check is invalid.");
public static final Errno ERRNO_FILE_NOT_READABLE = new Errno(TYPE, 401, "The %1$s at path is not readable. Permission Denied."); public static final Errno ERRNO_FILE_NOT_READABLE = new Errno(TYPE, 401, "The %1$s at path \"%2$s\" is not readable. Permission Denied.");
public static final Errno ERRNO_FILE_NOT_WRITABLE = new Errno(TYPE, 402, "The %1$s at path is not writable. Permission Denied."); public static final Errno ERRNO_FILE_NOT_READABLE_SHORT = new Errno(TYPE, 402, "The %1$s at path is not readable. Permission Denied.");
public static final Errno ERRNO_FILE_NOT_EXECUTABLE = new Errno(TYPE, 403, "The %1$s at path is not executable. Permission Denied."); public static final Errno ERRNO_FILE_NOT_WRITABLE = new Errno(TYPE, 403, "The %1$s at path \"%2$s\" is not writable. Permission Denied.");
public static final Errno ERRNO_FILE_NOT_WRITABLE_SHORT = new Errno(TYPE, 404, "The %1$s at path is not writable. Permission Denied.");
public static final Errno ERRNO_FILE_NOT_EXECUTABLE = new Errno(TYPE, 405, "The %1$s at path \"%2$s\" is not executable. Permission Denied.");
public static final Errno ERRNO_FILE_NOT_EXECUTABLE_SHORT = new Errno(TYPE, 406, "The %1$s at path is not executable. Permission Denied.");
FileUtilsErrno(final String type, final int code, final String message) { FileUtilsErrno(final String type, final int code, final String message) {
super(type, code, message); super(type, code, message);
} }
/** Defines the {@link Errno} mapping to get a shorter version of {@link FileUtilsErrno}. */
public static Map<Errno, Errno> ERRNO_SHORT_MAPPING = new HashMap<Errno, Errno>() {{
put(ERRNO_FILE_NOT_FOUND_AT_PATH, ERRNO_FILE_NOT_FOUND_AT_PATH_SHORT);
put(ERRNO_NON_REGULAR_FILE_FOUND, ERRNO_NON_REGULAR_FILE_FOUND_SHORT);
put(ERRNO_NON_DIRECTORY_FILE_FOUND, ERRNO_NON_DIRECTORY_FILE_FOUND_SHORT);
put(ERRNO_NON_SYMLINK_FILE_FOUND, ERRNO_NON_SYMLINK_FILE_FOUND_SHORT);
put(ERRNO_FILE_NOT_READABLE, ERRNO_FILE_NOT_READABLE_SHORT);
put(ERRNO_FILE_NOT_WRITABLE, ERRNO_FILE_NOT_WRITABLE_SHORT);
put(ERRNO_FILE_NOT_EXECUTABLE, ERRNO_FILE_NOT_EXECUTABLE_SHORT);
}};
} }

View File

@@ -11,6 +11,7 @@ public class FunctionErrno extends Errno {
public static final Errno ERRNO_NULL_OR_EMPTY_PARAMETERS = new Errno(TYPE, 101, "The %1$s parameters passed to \"%2$s\" are null or empty."); public static final Errno ERRNO_NULL_OR_EMPTY_PARAMETERS = new Errno(TYPE, 101, "The %1$s parameters passed to \"%2$s\" are null or empty.");
public static final Errno ERRNO_UNSET_PARAMETER = new Errno(TYPE, 102, "The %1$s parameter passed to \"%2$s\" must be set."); public static final Errno ERRNO_UNSET_PARAMETER = new Errno(TYPE, 102, "The %1$s parameter passed to \"%2$s\" must be set.");
public static final Errno ERRNO_UNSET_PARAMETERS = new Errno(TYPE, 103, "The %1$s parameters passed to \"%2$s\" must be set."); public static final Errno ERRNO_UNSET_PARAMETERS = new Errno(TYPE, 103, "The %1$s parameters passed to \"%2$s\" must be set.");
public static final Errno ERRNO_INVALID_PARAMETER = new Errno(TYPE, 104, "The %1$s parameter passed to \"%2$s\" is invalid.\"%3$s\"");
FunctionErrno(final String type, final int code, final String message) { FunctionErrno(final String type, final int code, final String message) {

View File

@@ -53,20 +53,25 @@ public class NotificationUtils {
* @param title The title for the notification. * @param title The title for the notification.
* @param notificationText The second line text of the notification. * @param notificationText The second line text of the notification.
* @param notificationBigText The full text of the notification that may optionally be styled. * @param notificationBigText The full text of the notification that may optionally be styled.
* @param pendingIntent The {@link PendingIntent} which should be sent when notification is clicked. * @param contentIntent The {@link PendingIntent} which should be sent when notification is clicked.
* @param deleteIntent The {@link PendingIntent} which should be sent when notification is deleted.
* @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}. * @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}.
* The builder returned will be {@code null} if {@link #NOTIFICATION_MODE_NONE} * The builder returned will be {@code null} if {@link #NOTIFICATION_MODE_NONE}
* is passed. That case should ideally be handled before calling this function. * is passed. That case should ideally be handled before calling this function.
* @return Returns the {@link Notification.Builder}. * @return Returns the {@link Notification.Builder}.
*/ */
@Nullable @Nullable
public static Notification.Builder geNotificationBuilder(final Context context, final String channelId, final int priority, final CharSequence title, final CharSequence notificationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) { public static Notification.Builder geNotificationBuilder(
final Context context, final String channelId, final int priority, final CharSequence title,
final CharSequence notificationText, final CharSequence notificationBigText,
final PendingIntent contentIntent, final PendingIntent deleteIntent, final int notificationMode) {
if (context == null) return null; if (context == null) return null;
Notification.Builder builder = new Notification.Builder(context); Notification.Builder builder = new Notification.Builder(context);
builder.setContentTitle(title); builder.setContentTitle(title);
builder.setContentText(notificationText); builder.setContentText(notificationText);
builder.setStyle(new Notification.BigTextStyle().bigText(notificationBigText)); builder.setStyle(new Notification.BigTextStyle().bigText(notificationBigText));
builder.setContentIntent(pendingIntent); builder.setContentIntent(contentIntent);
builder.setDeleteIntent(deleteIntent);
builder.setPriority(priority); builder.setPriority(priority);

View File

@@ -1,11 +1,17 @@
package com.termux.shared.packages; package com.termux.shared.packages;
import android.app.ActivityManager;
import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo; import android.content.pm.PackageInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.os.UserManager;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.termux.shared.R; import com.termux.shared.R;
import com.termux.shared.data.DataUtils; import com.termux.shared.data.DataUtils;
@@ -14,8 +20,7 @@ import com.termux.shared.logger.Logger;
import com.termux.shared.termux.TermuxConstants; import com.termux.shared.termux.TermuxConstants;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.util.List;
import javax.annotation.Nullable;
public class PackageUtils { public class PackageUtils {
@@ -120,15 +125,27 @@ public class PackageUtils {
} }
/** /**
* Get the {@code versionName} for the package associated with the {@code context}. * Check if the app associated with the {@code context} has {@link ApplicationInfo#FLAG_DEBUGGABLE}
* set.
* *
* @param context The {@link Context} for the package. * @param context The {@link Context} for the package.
* @return Returns the {@code versionName}. This will be {@code null} if an exception is raised. * @return Returns the {@code versionName}. This will be {@code null} if an exception is raised.
*/ */
public static Boolean isAppForPackageADebugBuild(@NonNull final Context context) { public static Boolean isAppForPackageADebuggableBuild(@NonNull final Context context) {
return ( 0 != ( context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE ) ); return ( 0 != ( context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE ) );
} }
/**
* Check if the app associated with the {@code context} has {@link ApplicationInfo#FLAG_EXTERNAL_STORAGE}
* set.
*
* @param context The {@link Context} for the package.
* @return Returns the {@code versionName}. This will be {@code null} if an exception is raised.
*/
public static Boolean isAppInstalledOnExternalStorage(@NonNull final Context context) {
return ( 0 != ( context.getApplicationInfo().flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE ) );
}
/** /**
* Get the {@code versionCode} for the package associated with the {@code context}. * Get the {@code versionCode} for the package associated with the {@code context}.
* *
@@ -163,7 +180,7 @@ public class PackageUtils {
* Get the {@code SHA-256 digest} of signing certificate for the package associated with the {@code context}. * Get the {@code SHA-256 digest} of signing certificate for the package associated with the {@code context}.
* *
* @param context The {@link Context} for the package. * @param context The {@link Context} for the package.
* @return Returns the{@code SHA-256 digest}. This will be {@code null} if an exception is raised. * @return Returns the {@code SHA-256 digest}. This will be {@code null} if an exception is raised.
*/ */
@Nullable @Nullable
public static String getSigningCertificateSHA256DigestForPackage(@NonNull final Context context) { public static String getSigningCertificateSHA256DigestForPackage(@NonNull final Context context) {
@@ -184,4 +201,219 @@ public class PackageUtils {
} }
} }
/**
* Get the serial number for the current user.
*
* @param context The {@link Context} for operations.
* @return Returns the serial number. This will be {@code null} if failed to get it.
*/
@Nullable
public static Long getSerialNumberForCurrentUser(@NonNull Context context) {
UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
if (userManager == null) return null;
return userManager.getSerialNumberForUser(android.os.Process.myUserHandle());
}
/**
* Check if the current user is the primary user. This is done by checking if the the serial
* number for the current user equals 0.
*
* @param context The {@link Context} for operations.
* @return Returns {@code true} if the current user is the primary user, otherwise [@code false}.
*/
public static boolean isCurrentUserThePrimaryUser(@NonNull Context context) {
Long userId = getSerialNumberForCurrentUser(context);
return userId != null && userId == 0;
}
/**
* Get the profile owner package name for the current user.
*
* @param context The {@link Context} for operations.
* @return Returns the profile owner package name. This will be {@code null} if failed to get it
* or no profile owner for the current user.
*/
@Nullable
public static String getProfileOwnerPackageNameForUser(@NonNull Context context) {
DevicePolicyManager devicePolicyManager = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
if (devicePolicyManager == null) return null;
List<ComponentName> activeAdmins = devicePolicyManager.getActiveAdmins();
if (activeAdmins != null){
for (ComponentName admin:activeAdmins){
String packageName = admin.getPackageName();
if(devicePolicyManager.isProfileOwnerApp(packageName))
return packageName;
}
}
return null;
}
/**
* Get the process id of the main app process of a package. This will work for sharedUserId. Note
* that some apps have multiple processes for the app like with `android:process=":background"`
* attribute in AndroidManifest.xml.
*
* @param context The {@link Context} for operations.
* @param packageName The package name of the process.
* @return Returns the process if found and running, otherwise {@code null}.
*/
@Nullable
public static String getPackagePID(final Context context, String packageName) {
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (activityManager != null) {
List<ActivityManager.RunningAppProcessInfo> processInfos = activityManager.getRunningAppProcesses();
if (processInfos != null) {
ActivityManager.RunningAppProcessInfo processInfo;
for (int i = 0; i < processInfos.size(); i++) {
processInfo = processInfos.get(i);
if (processInfo.processName.equals(packageName))
return String.valueOf(processInfo.pid);
}
}
}
return null;
}
/**
* Check if app is installed and enabled. This can be used by external apps that don't
* share `sharedUserId` with the an app.
*
* If your third-party app is targeting sdk `30` (android `11`), then it needs to add package
* name to the `queries` element or request `QUERY_ALL_PACKAGES` permission in its
* `AndroidManifest.xml`. Otherwise it will get `PackageSetting{...... package_name/......} BLOCKED`
* errors in `logcat` and `RUN_COMMAND` won't work.
* Check [package-visibility](https://developer.android.com/training/basics/intents/package-visibility#package-name),
* `QUERY_ALL_PACKAGES` [googleplay policy](https://support.google.com/googleplay/android-developer/answer/10158779
* and this [article](https://medium.com/androiddevelopers/working-with-package-visibility-dc252829de2d) for more info.
*
* {@code
* <manifest
* <queries>
* <package android:name="package_name" />
* </queries>
* </manifest>
* }
*
* @param context The context for operations.
* @return Returns {@code errmsg} if {@code packageName} is not installed or disabled, otherwise {@code null}.
*/
public static String isAppInstalled(@NonNull final Context context, String appName, String packageName) {
String errmsg = null;
PackageManager packageManager = context.getPackageManager();
ApplicationInfo applicationInfo;
try {
applicationInfo = packageManager.getApplicationInfo(packageName, 0);
} catch (final PackageManager.NameNotFoundException e) {
applicationInfo = null;
}
boolean isAppEnabled = (applicationInfo != null && applicationInfo.enabled);
// If app is not installed or is disabled
if (!isAppEnabled)
errmsg = context.getString(R.string.error_app_not_installed_or_disabled_warning, appName, packageName);
return errmsg;
}
/**
* Enable or disable a {@link ComponentName} with a call to
* {@link PackageManager#setComponentEnabledSetting(ComponentName, int, int)}.
*
* @param context The {@link Context} for operations.
* @param packageName The package name of the component.
* @param className The {@link Class} name of the component.
* @param state If component should be enabled or disabled.
* @param toastString If this is not {@code null} or empty, then a toast before setting state.
* @param showErrorMessage If an error message toast should be shown.
* @return Returns the errmsg if failed to set state, otherwise {@code null}.
*/
@Nullable
public static String setComponentState(@NonNull final Context context, @NonNull String packageName,
@NonNull String className, boolean state, String toastString,
boolean showErrorMessage) {
try {
PackageManager packageManager = context.getPackageManager();
if (packageManager != null) {
ComponentName componentName = new ComponentName(packageName, className);
if (toastString != null) Logger.showToast(context, toastString, true);
packageManager.setComponentEnabledSetting(componentName,
state ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
}
return null;
} catch (final Exception e) {
String errmsg = context.getString(
state ? R.string.error_enable_component_failed : R.string.error_disable_component_failed,
packageName, className) + ": " + e.getMessage();
if (showErrorMessage)
Logger.showToast(context, errmsg, true);
return errmsg;
}
}
/**
* Check if state of a {@link ComponentName} is {@link PackageManager#COMPONENT_ENABLED_STATE_DISABLED}
* with a call to {@link PackageManager#getComponentEnabledSetting(ComponentName)}.
*
* @param context The {@link Context} for operations.
* @param packageName The package name of the component.
* @param className The {@link Class} name of the component.
* @param logErrorMessage If an error message should be logged.
* @return Returns {@code true} if disabled, {@code false} if not and {@code null} if failed to
* get the state.
*/
public static Boolean isComponentDisabled(@NonNull final Context context, @NonNull String packageName,
@NonNull String className, boolean logErrorMessage) {
try {
PackageManager packageManager = context.getPackageManager();
if (packageManager != null) {
ComponentName componentName = new ComponentName(packageName, className);
// Will throw IllegalArgumentException: Unknown component: ComponentInfo{} if app
// for context is not installed or component does not exist.
return packageManager.getComponentEnabledSetting(componentName) == PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
}
} catch (final Exception e) {
if (logErrorMessage)
Logger.logStackTraceWithMessage(LOG_TAG, context.getString(R.string.error_get_component_state_failed, packageName, className), e);
}
return null;
}
/**
* Check if an {@link android.app.Activity} {@link ComponentName} can be called by calling
* {@link PackageManager#queryIntentActivities(Intent, int)}.
*
* @param context The {@link Context} for operations.
* @param packageName The package name of the component.
* @param className The {@link Class} name of the component.
* @param flags The flags to filter results.
* @return Returns {@code true} if it exists, otherwise {@code false}.
*/
public static boolean doesActivityComponentExist(@NonNull final Context context, @NonNull String packageName,
@NonNull String className, int flags) {
try {
PackageManager packageManager = context.getPackageManager();
if (packageManager != null) {
Intent intent = new Intent();
intent.setClassName(packageName, className);
return packageManager.queryIntentActivities(intent, flags).size() > 0;
}
} catch (final Exception e) {
// ignore
}
return false;
}
} }

View File

@@ -63,7 +63,11 @@ public class PermissionUtils {
result = ContextCompat.checkSelfPermission(activity, permission); result = ContextCompat.checkSelfPermission(activity, permission);
if (result != PackageManager.PERMISSION_GRANTED) { if (result != PackageManager.PERMISSION_GRANTED) {
Logger.logDebug(LOG_TAG, "Requesting Permissions: " + Arrays.toString(permissions)); Logger.logDebug(LOG_TAG, "Requesting Permissions: " + Arrays.toString(permissions));
activity.requestPermissions(new String[]{permission}, requestCode); try {
activity.requestPermissions(new String[]{permission}, requestCode);
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to request permissions with request code " + requestCode + ": " + Arrays.toString(permissions), e);
}
} }
} }
} }

View File

@@ -245,17 +245,22 @@ public class SharedPreferenceUtils {
* @param sharedPreferences The {@link SharedPreferences} to get the value from. * @param sharedPreferences The {@link SharedPreferences} to get the value from.
* @param key The key for the value. * @param key The key for the value.
* @param def The default value if failed to read a valid value. * @param def The default value if failed to read a valid value.
* @param defIfEmpty If set to {@code true}, then {@code def} will be returned if value is empty.
* @return Returns the {@code String} value stored in {@link SharedPreferences}, otherwise returns * @return Returns the {@code String} value stored in {@link SharedPreferences}, otherwise returns
* default if failed to read a valid value, like in case of an exception. * default if failed to read a valid value, like in case of an exception.
*/ */
public static String getString(SharedPreferences sharedPreferences, String key, String def) { public static String getString(SharedPreferences sharedPreferences, String key, String def, boolean defIfEmpty) {
if (sharedPreferences == null) { if (sharedPreferences == null) {
Logger.logError(LOG_TAG, "Error getting String value for the \"" + key + "\" key from null shared preferences. Returning default value \"" + def + "\"."); Logger.logError(LOG_TAG, "Error getting String value for the \"" + key + "\" key from null shared preferences. Returning default value \"" + def + "\".");
return def; return def;
} }
try { try {
return sharedPreferences.getString(key, def); String value = sharedPreferences.getString(key, def);
if (defIfEmpty && (value == null || value.isEmpty()))
return def;
else
return value;
} }
catch (ClassCastException e) { catch (ClassCastException e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Error getting String value for the \"" + key + "\" key from shared preferences. Returning default value \"" + def + "\".", e); Logger.logStackTraceWithMessage(LOG_TAG, "Error getting String value for the \"" + key + "\" key from shared preferences. Returning default value \"" + def + "\".", e);

Some files were not shown because too many files have changed in this diff Show More