Compare commits

..

129 Commits
v0.72 ... v0.95

Author SHA1 Message Date
Leonid Pliushch
2224d917a3 update ndk version 2020-06-17 14:08:58 +03:00
Leonid Pliushch
664ec43f94 version 0.95 2020-06-09 23:08:23 +03:00
Leonid Pliushch
6cf742460c extra keys: make popup & pressed button no longer transparent
Fixes visual issue when popup overlaps text of upper row.
2020-06-09 22:54:57 +03:00
Leonid Pliushch
72981fb981 Use vector drawables for text selection handle
https://github.com/termux/termux-app/issues/1036
2020-06-09 21:47:11 +03:00
Leonid Pliushch
2c5534e2c1 RunCommandService: update information about usage 2020-06-09 16:07:42 +03:00
Leonid Pliushch
5b32540635 minor refactoring: RunCommand => RunCommandService 2020-06-09 15:48:56 +03:00
Leonid Pliushch
db3ff7b24a Provide a service for executing commands by third-party applications
Re-implementation of https://github.com/termux/termux-app/pull/1029.

If Termux has property "allow-external-apps" set to "true", a third-party
program will be able to send intents for executing custom commands
within Termux environment.

Third-party program must declare permission "com.termux.permission.RUN_COMMAND".
2020-06-09 15:36:08 +03:00
Trygve Aaberge
5f71e3e73a Join lines that fills the width when selecting urls
Some terminal applications, like mutt and weechat, prints a newline at
the end of each line even if text is wrapped. This causes urls which are
wrapped to not be selectable in full.

By ignoring newlines when the text fills the entire width of the screen,
those urls can be selected. Many other terminal emulators do this as
well.

A drawback of this is that if a url happens to fill the width of the
screen, the url selection will include the first word of the next line,
but this doesn't happen that often so I think it's an okay tradeoff.

Fixes #313
2020-06-09 13:41:36 +03:00
Josh Home
3e04ea4cb0 Add Log file to view intent #1497 2020-06-09 12:31:59 +03:00
Trygve Aaberge
0af823607a Improvements to extra keys (#1479)
* Make popup keys for extra keys row configurable

This makes the keys you get when swiping up on a key configurable. You
can configure such a key by using an array of strings instead of a
single string in the row. The first entry will be the normal key and the
second will be the extra key.

This is a slightly breaking change, as people that have configured
custom extra keys with "-" or "/" will have to change the config to keep
the popup keys. The default config will remain the same in terms of
functionality, i.e. it includes the same popup key for "-".

* Make popup keys interact well with long press keys

This stops the repeat action when the popup is shown, and makes sure the
popup is closed when you release even if there has been some repeat
actions.

* Support configuring the style of the extra keys

This adds a setting for choosing between the different ways to render
key names that were already present in ExtraKeysView.

The available setting values are "arrows-only", "arrows-all", "all",
"none" and "default". Other values will fallback to "default".

Can be used as a workaround for #1410

* Support using modifier keys with letter keys in extra keys

This allows you to use the modifier keys on the extra keys rows, e.g.
ctrl, together with another button on the extra keys rows, as long as
that button is a normal letter and not a special key. Support for
special keys will come in the next commit.

* Support using modifier keys with special keys in extra keys

This allows you to use the modifier keys on the extra keys rows together
with a special key on the extra keys rows, e.g. CTRL+LEFT.

Fixes #745, fixes most of #895 and possibly #154

* Support mapping extra keys to other actions

This adds a setting called extra-keys-map which allows you to map a key
on the extra keys rows to another action. The value is a json object
where the key is the button text as configured in extra-keys and the
value is the action. Multiple actions can be used, but if they are
special characters (like ESC or LEFT) they have to be separated from the
other characters with a space on each side. If you want an actual space
character, use SPACE.

For example if you want to add a key to go to the next active channel in
weechat, you can use this:
    extra-keys-map = {"weechat next": "ESC a"}
And then add "weechat next" to extra-keys. The name can of course be
whatever you want.

Or if you want the button for the UP arrow to show ⇧ instead of ↑, you
can use this:
    extra-keys-map = {"⇧": "UP"}
And put "⇧" in extra-keys instead of "UP".

Modifier keys (ctrl, alt and shift) can't be used in this map yet.
Support for ctrl and alt will come in the next commit.

I think this fixes #1186

* Support CTRL and ALT in extra keys map

This allows you to use CTRL and ALT in extra-keys-map.

For example if you want a button to exit the terminal, you can use this:

    extra-keys-map = {"exit": "CTRL d"}

And add "exit" to extra-keys.

* Support a KEYBOARD button in extra keys

This toggles showing the keyboard input method.

* Support specifying macro keys in the extra-keys option

Instead of specifying macros in the separate extra-keys-map option by
matching the key name in the two options, you can now use "macro"
instead of "key" in extra-keys, and it will be a macro, i.e. a sequence
of multiple keys separated by space.

* Remove option extra-keys-map

Now that you can specify macro in extra-keys, there is no point in
having this separate option. Instead of specifying the value to display
as key, and the macro to perform in extra-keys-map, you would now
specify the value to display in the display property and the macro to
perform in the macro property.

* Lookup display text when creating ExtraKeyButton

This will make it easier to support key aliases for macros in the next
commit.

* Add support for a key to open the drawer

Fixes (I think) #1325
2020-06-09 12:17:07 +03:00
Trygve Aaberge
4d9c0c315e Update notification icon to match launcher icon
This is the launcher icon with a circle around it. I added the circle
because the icon has a transparent background, so it looks a bit weird
with just the >_.
2020-06-09 12:11:49 +03:00
Leonid Pliushch
9c32935ca2 fix ndk version for terminal-emulator module 2020-06-09 12:08:13 +03:00
Leonid Pliushch
669c336e2f update ndk version 2020-06-09 12:02:41 +03:00
Trygve Aaberge
35842cf4a6 Set orientation of HandleView in show (#1477)
* Place long press menu above selection

Previously, the long press menu would cover the first line of the
selection.

* Flip selection handle at different positions depending on drag direction

When the selection handle changes direction, the selection jumps to the
new point of the handle. When the handle changes direction at the same
place when you come from the left as from the right, that makes it
impossible to select the characters which are at the position where it
changes direction.

With this change the handle remains pointing towards the edge further
into the line when you drag it from the edge and against the center.

* Set orientation of HandleView when showing it

When you hold down on a word that starts or ends at the edge of the
screen, the handle will appear outside of the screen. This happens
because the orientation was only switched when the handle is dragged, so
when it is shown it just used the same orientation as it had for the
last selection.

Relates to #334, but not sure if it fixes it completely.
2020-06-09 11:59:59 +03:00
Trygve Aaberge
b086270a5a Support auto detection of dark theme
By default it uses the system setting. If use-black-ui is set to either
true or false, that overrides it.

Fixes #1351 properly, fixes #1354
2020-06-09 11:59:07 +03:00
Leonid Pliushch
f39f06a540 update bootstraps again
* Reduced archive size by using nano as preinstalled text editor instead of vim.
* Updated command-not-found and termux-tools.
2020-06-07 22:36:14 +03:00
Leonid Pliushch
58440bc88d update bootstraps again
Added few more packages to bootstraps which are replacements to
some busybox applets.
2020-06-07 03:56:51 +03:00
Leonid Pliushch
9f438e2912 update bootstraps 2020-06-06 22:42:29 +03:00
Leonid Pliushch
f794bfcadc Handle all exceptions when loading termux.properties (#1590)
* Catch all exceptions which can occur.
* Print short description in toast message about occurred exception.
2020-06-06 21:20:05 +03:00
hannesa2
b6d7831646 Fix CI build and see result in pull request (#1565) 2020-05-24 02:05:24 +02:00
Leonid Pliushch
d212198e30 update bootstraps 2020-05-24 02:44:06 +03:00
Fredrik Fornwall
c2843897ac Fix build by specifying ndkVersion 2020-05-24 01:27:01 +02:00
Fredrik Fornwall
c4c4912a7e Avoid specifying ndk version 2020-05-24 01:22:33 +02:00
Hannes Achleitner
38a3319ca2 show result in pull request 2020-05-24 01:08:01 +02:00
Hannes Achleitner
7e13b8aa2e Verify Gradle 2020-05-24 01:08:01 +02:00
Frieder Bluemle
6dca19ae00 Update Android Gradle plugin to 3.6.3 2020-05-24 01:06:15 +02:00
Leonid Plyushch
2659c06c5d Remove irrelevant message from bootstrap installation error
Bootstraps are no longer downloaded from the Internet.
2020-04-30 14:34:35 +03:00
Fredrik Fornwall
9b7c7102b2 Bump version to 0.94
Some checks failed
Build / build (push) Has been cancelled
Unit tests / testing (push) Has been cancelled
2020-03-24 23:25:49 +01:00
Daniel Lublin
1819087ca0 Add tests for URL fragment 2020-03-24 23:13:14 +01:00
Daniel Lublin
366a61f052 Grab fragment when extracting URLs for selection 2020-03-24 23:13:14 +01:00
Leonid Plyushch
6e224cabcf set ndk version in build.gradle
Fixing CI builds.
2020-03-21 11:31:52 +02:00
Leonid Plyushch
8c8fa96133 update bootstraps 2020-03-21 10:35:17 +02:00
Fredrik Fornwall
537f2ed97e Update gradle wrapper 2020-03-20 14:42:43 +01:00
Fredrik Fornwall
0e23315c41 Add android.useAndroidX=true to gradle.properties 2020-03-20 10:44:24 +01:00
Fredrik Fornwall
2cde986419 Update Android gradle plug-in 2020-03-16 13:58:34 +01:00
Fredrik Fornwall
d28939810c Update Robolectric from 4.3 to 4.3.1 2020-02-21 14:02:53 +01:00
Fredrik Fornwall
93724b7aa6 Bump version to 0.92
Some checks failed
Build / build (push) Has been cancelled
Unit tests / testing (push) Has been cancelled
2020-02-09 22:59:33 +01:00
x0b
5d06f040e8 Fix deletable flags in DocumentProvider
A file is deletable if the _parent_ is writable, not the file itself.
2020-02-09 22:57:27 +01:00
x0b
ed9afa082a Fix #1350: Support createDocument(...) 2020-02-09 22:57:27 +01:00
x0b
9703bd31ad Fix #1424: Support ACTION_OPEN_DOCUMENT_TREE 2020-02-09 22:57:27 +01:00
Leonid Plyushch
9749f25eba update bootstrap archives
v19 had a problem with metadata which broke the package manager.
2020-02-09 12:34:11 +02:00
Leonid Plyushch
453b838b24 update bootstrap archives 2020-02-08 20:33:34 +02:00
Leonid Plyushch
9fdf2a49fd update readme 2020-02-07 15:48:32 +02:00
Leonid Plyushch
3270506bff update issue templates 2020-02-07 15:05:16 +02:00
Leonid Plyushch
a240f4cf45 CI: fix job name in run_tests.yml 2020-02-07 14:02:34 +02:00
Leonid Plyushch
4647beb0d2 update readme 2020-02-07 13:57:08 +02:00
Leonid Plyushch
d92e806461 CI: update workflow labels 2020-02-07 13:55:45 +02:00
Leonid Plyushch
b75cf0bb84 CI: add configuration for running terminal emulator unit tests 2020-02-07 13:50:12 +02:00
Leonid Plyushch
f928efed4e CI: switch to Github Actions 2020-02-07 13:47:22 +02:00
Fredrik Fornwall
36db64d585 Bump version to 0.90 2020-02-02 03:36:52 +01:00
Jean Schurger
b8f0430699 Prevent kill by Samsung Dex on plug/unplug. 2020-02-02 03:36:13 +01:00
Edontin
90e6260d5e Allow the user to disable virtual key emulation.
Use volume-keys=volume within termux.properties to disable.
2020-01-06 10:21:32 +01:00
Fredrik Fornwall
566d656c16 Avoid trailing slash in CWD (fixes #1413) 2020-01-05 19:14:52 +01:00
Fredrik Fornwall
b729085d52 Bump version to 0.88 2020-01-05 19:14:35 +01:00
Fredrik Fornwall
a94bcb02f1 Bump version to 0.86 2020-01-05 02:02:05 +01:00
Fredrik Fornwall
e28be01dc2 Create new terminal sessions with directory of active session
This mimics the behaviour of most tabbed terminal emulators.

Fixes #1009.
2020-01-05 02:00:25 +01:00
Archenoth
7f7c1efac1 Add PendingIntent in TermuxService to return data from execution
This commit adds an optional final argument to the BackgroundJob
constructor for a PendingIntent to return the results of its
execution to, and also attempts to pass an optional pendingIntent to
it from the Service start intent
2020-01-05 01:07:11 +01:00
Archenoth
76df44e6bb Start stderr BackgroundJob Thread 2020-01-05 01:07:11 +01:00
Fredrik Fornwall
fd13f3f98d Handle magnet links with termux-url-opener
Fixes #1339 and #1382.
2020-01-05 01:02:47 +01:00
Leonid Plyushch
269c3cafb0 properly set bootclasspath environment variable 2020-01-05 00:38:49 +01:00
Fredrik Fornwall
662b5cace6 Update gradle wrapper 2020-01-01 22:57:52 +01:00
Fredrik Fornwall
3c01b09fff Bump version to 0.85 2019-12-29 18:53:00 +01:00
Fredrik Fornwall
1ce2db9929 Merge branch 'maoabc-master' 2019-12-29 18:50:34 +01:00
Fredrik Fornwall
c987ff718a Update Android Gradle plug-in 2019-12-29 18:49:41 +01:00
Leonid Plyushch
9e295b105c terminal emulator: clear scrollback buffer when resetting to initial state
Fixes utility 'reset' not being able to clear scrollback buffer.
2019-12-29 18:49:41 +01:00
Fredrik Fornwall
37b3a9f8db Update Android Gradle plug-in 2019-12-29 18:49:10 +01:00
Leonid Plyushch
490853e427 terminal emulator: clear scrollback buffer when resetting to initial state
Fixes utility 'reset' not being able to clear scrollback buffer.
2019-12-24 00:32:40 +01:00
mao
9ebedc4740 Merge remote-tracking branch 'remotes/origin/master' 2019-12-19 00:43:45 +08:00
mao
82730d9e08 confict 2019-12-19 00:34:14 +08:00
mao
3d2756f376 Optimize handleView move. 2019-12-18 23:50:23 +08:00
Leonid Plyushch
6db51bdec0 remove SDK_INT conditionals
We are targting on API 24, so conditionals for Android 5/6 compatibillity
are useless now.
2019-12-18 14:06:06 +02:00
mao
c238c8bddb Optimize handle view 2019-12-18 14:00:58 +02:00
mao
60e1556871 Stop selection mode on enter 2019-12-18 14:00:57 +02:00
mao
e47bd31681 Selection mode fling 2019-12-18 14:00:57 +02:00
mao
951df6b4e2 Add selection mode cursor controller 2019-12-18 14:00:32 +02:00
Leonid Plyushch
fcd3bc1133 update gradle wrapper 2019-12-02 17:19:17 +02:00
Fredrik Fornwall
f4db9ff212 Bump version to 0.84 2019-11-24 19:10:10 +01:00
Leonid Plyushch
87841886d4 extra keys: improve handling of DnD mode
Do not disturb mode is now handled only for SDKs pre-28. Extra keys will
not vibrate only when total silence mode is used.
2019-11-24 19:08:32 +01:00
Leonid Plyushch
cf883f5f05 extra keys: handle actions UP & CANCEL separately
Related issue: https://github.com/termux/termux-app/issues/905
2019-11-24 19:08:32 +01:00
Fredrik Fornwall
677d75e173 Remove outdated tests
Remove tests that asserted that Cursor Down (CUD) and Cursor Up (CUU)
escape sequences were affected by the scrolling region set by DECSTBM.

This was incorrect and recently fixed:
https://github.com/termux/termux-app/issues/1340
2019-11-24 19:07:34 +01:00
Fredrik Fornwall
cdccc2c433 Do not limit cursor movement to scroll region
The scrolling region set by DECSTBM should not affect the
Cursor Down (CUD) and Cursor Up (CUU) escape sequences.

Fixes #1340.
2019-11-10 22:05:06 +01:00
Fredrik Fornwall
070436a6ed Bump version to 0.82 2019-11-10 19:26:10 +01:00
Fredrik Fornwall
69ded4b33e Update Android gradle plug-in 2019-11-10 19:26:01 +01:00
Fredrik Fornwall
e5a8c0eb4a Cleanup after requiring Android 7.0 2019-11-03 22:39:21 +01:00
Fredrik Fornwall
29815fb3f0 Fix gradle deprecation warning 2019-11-03 22:35:52 +01:00
Fredrik Fornwall
e930ac08aa Bump version to 0.78 2019-11-03 19:18:31 +01:00
Fredrik Fornwall
3b969a0077 Bump version to 0.77 2019-10-28 01:54:50 +01:00
Leonid Plyushch
ae717d8f5f do not allow to backup data by Google
Why:

 * During backup process Termux is being killed in most cases.
 * Backup data is limited to 25 MB.
 * Backup may not be performed/updated in certain cases.
2019-10-27 20:33:12 +01:00
Leonid Plyushch
6c55c3c1be bootstraps: update to v18 2019-10-20 22:06:18 +03:00
Fredrik Fornwall
c0a0822843 Do not set LD_LIBRARY_PATH by default
See https://github.com/termux/termux-app/issues/1286
2019-10-20 20:48:10 +02:00
Fredrik Fornwall
08b08d1c47 Bump version to 0.76 2019-10-20 20:16:48 +02:00
Fredrik Fornwall
c76cec186a Include bootstrap zips as shared libraries 2019-10-20 20:15:50 +02:00
Leon Omelan
5ba3f7cf6d Made Black UI an option to configure 2019-10-20 20:05:48 +02:00
Leon Omelan
468f878a38 Unified UI colors across the app. Dark sidebar and dark app theme for
dark Alert Dialogs
2019-10-20 20:05:48 +02:00
Fredrik Fornwall
c50a367063 Add .cxx folder to gitignore 2019-10-13 20:48:24 +02:00
Leonid Plyushch
4189f598b9 add permission ACCESS_NETWORK_STATE
Seems to be required by some Android TV devices.
2019-10-13 20:05:55 +02:00
mao
fdb3764f5c Optimize handle view 2019-10-11 07:37:57 +08:00
Leonid Plyushch
cb67e805de update readme 2019-10-07 15:32:21 +03:00
Leonid Plyushch
3f83368f42 CI: update .cirrus.yml 2019-10-07 15:01:22 +03:00
Leonid Plyushch
8c7462678f update readme 2019-10-07 00:21:57 +03:00
Leonid Plyushch
6f11390cc4 update readme 2019-10-07 00:20:33 +03:00
Leonid Plyushch
33d390a228 CI: development builds are now signed with publically-shared key
This commit adds keystore which is shared with community. People freely can
use it for creating own Termux app and plugin builds. However, builds signed
with such keystore cannot be proven as official.

Signed-off-by: Leonid Plyushch <leonid.plyushch@gmail.com>
2019-10-06 23:24:15 +03:00
mao
937eb350b2 Stop selection mode on enter 2019-10-05 18:54:15 +08:00
mao
3b4ece6bd8 Selection mode fling 2019-10-05 18:30:54 +08:00
mao
35a4fdacbe Add selection mode cursor controller 2019-10-05 18:05:42 +08:00
Leonid Plyushch
0332779d6a update readme 2019-10-04 23:50:46 +03:00
Leonid Plyushch
f19575b942 readme: update status badge url 2019-10-04 02:16:22 +03:00
Leonid Plyushch
6a95809da3 CI: add test keystore 2019-10-04 02:08:44 +03:00
Leonid Plyushch
ac8dab70ef CI: add initial configuration for Cirrus 2019-10-04 01:59:49 +03:00
Leonid Plyushch
243285f7c2 CI: remove Travis setup 2019-10-04 01:59:49 +03:00
Fredrik Fornwall
b6182216d5 Update android gradle plug-in 2019-10-02 21:37:23 +02:00
Fredrik Fornwall
e206121bbc Bump version to 0.75 2019-09-08 21:43:52 +02:00
Fredrik Fornwall
deceffad00 Update gradle configuration 2019-09-08 21:43:07 +02:00
Leonid Plyushch
c19909cef1 improve url matching regex
Now it support complex URLs and some other schemes beyond just http/ftp.
2019-09-08 20:10:26 +02:00
Leonid Plyushch
5b7e40638c handle possible ActivityNotFoundException when requesting to disable battery optimizations
Related issue: https://github.com/termux/termux-app/issues/1224
2019-09-08 20:10:15 +02:00
Leonid Plyushch
a3673d1af5 extra keys: follow DnD mode and haptic feedback Android settings
Do not vibrate when:

 * Do not disturb mode is on.

 * Haptic feedback is disabled in Android settings.
2019-09-08 20:08:16 +02:00
Fredrik Fornwall
d5f9cf85c9 Bump version to 0.74 2019-08-18 22:11:21 +02:00
hungphd
549f09573f Change method name 'with' to 'getInstance' 2019-08-18 20:30:34 +02:00
Daniil Gentili
94e5bc86fb Ignore battery optimization settings to prevent suspension 2019-08-18 20:23:00 +02:00
Fredrik Fornwall
370ac2bd89 Avoid joining lines for shared transcript (#1166) 2019-08-04 19:42:36 +02:00
Fredrik Fornwall
7041f41981 Bump version to 0.73 2019-08-04 17:03:41 +02:00
Fredrik Fornwall
dd6a21257b Update the Android Gradle plugin 2019-08-04 16:44:58 +02:00
Fredrik Fornwall
445da0c4ea Fix problem with sharing large terminal transcript
Fixes #1166.
2019-07-14 20:26:16 +02:00
Sacha Chua
92980824b1 Add function keys F1 to F12 as keyboard options 2019-07-04 23:27:13 +02:00
Michal Bednarski
7d42b07a32 Also export ANDROID_TZDATA_ROOT
Since Android Q Beta 2 ANDROID_TZDATA_ROOT is required
in addition to ANDROID_RUNTIME_ROOT in order to run
`am` and `dalvikvm`
2019-07-04 23:26:55 +02:00
Fredrik Fornwall
e502b9169f Respect failsafe sessions in ACTION_EXECUTE 2019-06-09 20:00:17 +02:00
Leonid Plyushch
9f2d887ca0 minor fix for log message while removing tmpdir 2019-06-09 14:31:09 +03:00
Fredrik Fornwall
e6aacc5f08 Update Android Gradle plug-in 2019-05-27 14:07:50 +02:00
Fredrik Fornwall
ff935be54a Add versionName gradle task 2019-05-27 00:07:41 +02:00
56 changed files with 2187 additions and 626 deletions

View File

@@ -1,20 +1,35 @@
---
name: Bug report
about: Create a report to help us improve termux-app
about: Create a report to help us improve Termux application
---
<!-- Important note: Refusing to provide needed information may result in issue closing. If you are having problems with a package in termux then a bug report should be filed in the termux-packages repo: https://github.com/termux/termux-packages -->
<!--
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 with the termux app is. You may post screenshots in addition to description.
<!--
A clear and concise description of what the problem is.
You may post screenshots in addition to description.
-->
**Steps to reproduce**
Please post all steps that are needed to reproduce the issue.
<!--
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**
Post output of command `termux-info`.
If you are rooted or have access to adb then capture a logcat with `logcat -d "*:W"`, from a adb or root shell.
* Termux application version:
* Android OS version:
* Device model:

View File

@@ -1,12 +1,22 @@
---
name: Feature request
about: Suggest a new feature in termux-app
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
Provide links to more background information.

24
.github/workflows/debug_build.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Build
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v2
- name: Build
run: |
./gradlew assembleDebug
- name: Store generated APK file
uses: actions/upload-artifact@v1
with:
name: termux-app
path: ./app/build/outputs/apk/debug/app-debug.apk

View File

@@ -0,0 +1,17 @@
name: "Validate Gradle Wrapper"
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
validation:
name: "Validation"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: gradle/wrapper-validation-action@v1

19
.github/workflows/run_tests.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: Unit tests
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
testing:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v2
- name: Execute tests
run: |
./gradlew test

2
.gitignore vendored
View File

@@ -7,6 +7,8 @@ build/
*.apk
*.so
.externalNativeBuild
.cxx
*.zip
# Crashlytics configuations
com_crashlytics_export_strings.xml

View File

@@ -1,34 +0,0 @@
sudo: false
language: android
jdk: oraclejdk8
env:
global:
# The next declaration is the encrypted COVERITY_SCAN_TOKEN, created
# via the "travis encrypt" command using the project repo's public key
- secure: "LdajbHNfRlpnqzhX5KY2Vr7KtzU9vXDs1TCNn93J6Dt522f2AaiyUDJvISvz+uslk0WJiS5bB5vGwQmXginxz6Qi6uMgMbjWXulv1vfs6ZviKpUX348DOp1qKPa8WfVNB66F84SwGIfc8cRMAgCFw79l/DFgLErubF8vKo1wZ8Hmvrz//+RJ0BGMa3YRc4VyJhAL0P+0Wc1Q2Im7R9EovAxC5pZXBIMSgr6g5GzLWPisbNLXpMPGsDeYhcenO6XCtCCy+aNxUYM8vcrLDzlVXR5Hy7KEs/MGRTS0Yk13TWUEYa5wBpKelFTszdWYLVn5ANreh/aXRVfHpnW3epotMYguLx1kSvOhWEnc4F+qqv3nle2LpDg9Y9bcLyTTcYnPl9smqEVVjEDu0FoIr1V58xkG4Oc6BPIvLRjlMVU96PXh2HxMLuGsJ/xM+uAFU9oVMbC07xn42Eu5O4NHOHJNOwMWac4/lSKRK8W/7/vWuXj5vhkD9ZsGVpN70UtY5HAfNUGADnTeDblvjgFTNZ2mUN/u0o7Z8ZFURYllZ9YU+Vr2nPf9CAhVBjuwFWx8uRQpAg1aDmc1dVMJijRBeBeU/uWhYqsGp34wkNEl8VGzob4R4QTyI8+T7CndGqKVmbTK/SjqKhjjPpbXIAfOH+JtxvAnNmb8XeQSJ32uK2nexFo="
android:
components:
- platform-tools
- tools
- build-tools-28.0.3
- android-28
- extra-android-m2repository
before_install:
- yes | sdkmanager "ndk-bundle"
- yes | sdkmanager "platforms;android-28"
script:
- ./gradlew testDebugUnitTest
addons:
coverity_scan:
project:
name: "termux/termux-app"
description: "Terminal emulator and Linux environment for Android"
notification_email: fredrik@fornwall.net
build_command_prepend: "./gradlew clean"
build_command: "./gradlew build"
branch_pattern: master

View File

@@ -1,32 +1,65 @@
[![Travis build status](https://travis-ci.org/termux/termux-app.svg?branch=master)](https://travis-ci.org/termux/termux-app)
# Termux application
[![Build status](https://github.com/termux/termux-app/workflows/Build/badge.svg)](https://github.com/termux/termux-app/actions)
[![Testing status](https://github.com/termux/termux-app/workflows/Unit%20tests/badge.svg)](https://github.com/termux/termux-app/actions)
[![Join the chat at https://gitter.im/termux/termux](https://badges.gitter.im/termux/termux.svg)](https://gitter.im/termux/termux)
[Termux](https://termux.com) is an Android terminal application and Linux environment.
Termux app
==========
- [Termux Reddit community](https://reddit.com/r/termux)
- [Termux Wiki](https://wiki.termux.com/wiki/)
- [Termux Twitter](http://twitter.com/termux/)
[Termux](https://termux.com) is an Android terminal app and Linux environment.
Note that this repository is for the app itself (the user interface and the
terminal emulation). For the packages installable inside the app, see
[termux/termux-packages](https://github.com/termux/termux-packages)
* [Termux on Google Play Store](https://play.google.com/store/apps/details?id=com.termux)
* [Termux on F-Droid](https://f-droid.org/repository/browse/?fdid=com.termux)
* [Termux Google+ community](http://termux.com/community/)
* [Termux Wiki](https://wiki.termux.com/wiki/)
* [Termux Twitter](http://twitter.com/termux/)
## Installation
Note that this repository is for the app itself (the user interface and the terminal emulation). For the packages installable inside the app, see [termux/termux-packages](https://github.com/termux/termux-packages)
Termux:Widget application can be obtained from:
Terminal resources
==================
* [XTerm control sequences](http://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
* [vt100.net](http://vt100.net/)
* [Terminal codes (ANSI and terminfo equivalents)](http://wiki.bash-hackers.org/scripting/terminalcodes)
- [Google Play](https://play.google.com/store/apps/details?id=com.termux)
- [F-Droid](https://f-droid.org/en/packages/com.termux/)
- [Kali Nethunter Store](https://store.nethunter.com/en/packages/com.termux/)
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).
* 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)).
* 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).
* xterm: The grandfather of terminal emulators. [Source](http://invisible-island.net/datafiles/release/xterm.tar.gz).
* 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).
Additionally we provide per-commit debug builds for those who want to try
out the latest features or test their pull request. This build can be obtained
from one of the workflow runs listed on [Github Actions](https://github.com/termux/termux-app/actions)
page.
Signature keys of all offered builds are different. Before you switch the
installation source, you will have to uninstall the Termux application and
all currently installed plugins.
## Terminal resources
- [XTerm control sequences](http://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
- [vt100.net](http://vt100.net/)
- [Terminal codes (ANSI and terminfo equivalents)](http://wiki.bash-hackers.org/scripting/terminalcodes)
## 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).
- 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)).
- 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).
- xterm: The grandfather of terminal emulators.
[Source](http://invisible-island.net/datafiles/release/xterm.tar.gz).
- 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).

View File

@@ -1,10 +1,13 @@
apply plugin: 'com.android.application'
plugins {
id "com.android.application"
}
android {
compileSdkVersion 28
ndkVersion '21.3.6528147'
dependencies {
implementation "androidx.annotation:annotation:1.0.1"
implementation "androidx.annotation:annotation:1.1.0"
implementation "androidx.viewpager:viewpager:1.0.0"
implementation "androidx.drawerlayout:drawerlayout:1.0.0"
implementation project(":terminal-view")
@@ -12,10 +15,30 @@ android {
defaultConfig {
applicationId "com.termux"
minSdkVersion 21
minSdkVersion 24
targetSdkVersion 28
versionCode 72
versionName "0.72"
versionCode 95
versionName "0.95"
externalNativeBuild {
ndkBuild {
cFlags "-std=c11", "-Wall", "-Wextra", "-Werror", "-Os", "-fno-stack-protector", "-Wl,--gc-sections"
}
}
ndk {
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
}
}
signingConfigs {
debug {
storeFile file('dev_keystore.jks')
keyAlias 'alias'
storePassword 'xrj45yWGLbsO7W0v'
keyPassword 'xrj45yWGLbsO7W0v'
}
}
buildTypes {
@@ -24,14 +47,102 @@ android {
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
signingConfig signingConfigs.debug
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
externalNativeBuild {
ndkBuild {
path "src/main/cpp/Android.mk"
}
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
dependencies {
testImplementation 'junit:junit:4.12'
testImplementation 'junit:junit:4.13'
testImplementation 'org.robolectric:robolectric:4.3.1'
}
task versionName {
doLast {
print android.defaultConfig.versionName
}
}
def downloadBootstrap(String arch, String expectedChecksum, int version) {
def digest = java.security.MessageDigest.getInstance("SHA-256")
def localUrl = "src/main/cpp/bootstrap-" + arch + ".zip"
def file = new File(projectDir, localUrl)
if (file.exists()) {
def buffer = new byte[8192]
def input = new FileInputStream(file)
while (true) {
def readBytes = input.read(buffer)
if (readBytes < 0) break
digest.update(buffer, 0, readBytes)
}
def checksum = new BigInteger(1, digest.digest()).toString(16)
if (checksum == expectedChecksum) {
return
} else {
logger.quiet("Deleting old local file with wrong hash: " + localUrl)
file.delete()
}
}
def remoteUrl = "https://bintray.com/termux/bootstrap/download_file?file_path=bootstrap-" + arch + "-v" + version + ".zip"
logger.quiet("Downloading " + remoteUrl + " ...")
file.parentFile.mkdirs()
def out = new BufferedOutputStream(new FileOutputStream(file))
def connection = new URL(remoteUrl).openConnection()
connection.setInstanceFollowRedirects(true)
def digestStream = new java.security.DigestInputStream(connection.inputStream, digest)
out << digestStream
out.close()
def checksum = new BigInteger(1, digest.digest()).toString(16)
if (checksum != expectedChecksum) {
file.delete()
throw new GradleException("Wrong checksum for " + remoteUrl + ": expected: " + expectedChecksum + ", actual: " + checksum)
}
}
clean {
doLast {
def tree = fileTree(new File(projectDir, 'src/main/cpp'))
tree.include 'bootstrap-*.zip'
tree.each { it.delete() }
}
}
task downloadBootstraps(){
doLast {
def version = 25
downloadBootstrap("aarch64", "633baa1f7edfd81f6064338a68d1149aa203d4b24cbc4f7c64283aaca109609e", version)
downloadBootstrap("arm", "a581a22e0d79a0e8cef9395b1bd951ba066ac2d688522e17cca0b3e1c0649daa", version)
downloadBootstrap("i686", "8288e13f0a6ddeb2ff9406d8f968a8930a58e9318d09fadb2b7c8970a034cfdc", version)
downloadBootstrap("x86_64", "c99b80a18d6bbb64c24c5a64d6ee6b8d4306729ebd172662b807bcb4a46dd39a", version)
}
}
afterEvaluate {
android.applicationVariants.all { variant ->
variant.javaCompileProvider.get().dependsOn(downloadBootstraps)
}
}

BIN
app/dev_keystore.jks Normal file

Binary file not shown.

View File

@@ -8,16 +8,23 @@
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<uses-feature android:name="android.software.leanback" android:required="false" />
<permission android:name="com.termux.permission.RUN_COMMAND"
android:label="@string/run_command_permission_label"
android:description="@string/run_command_permission_description"
android:icon="@drawable/ic_launcher"
android:protectionLevel="dangerous" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<application
android:extractNativeLibs="true"
android:allowBackup="true"
android:fullBackupContent="@xml/backupscheme"
android:allowBackup="false"
android:icon="@drawable/ic_launcher"
android:banner="@drawable/banner"
android:label="@string/application_name"
@@ -30,11 +37,11 @@
<activity
android:name="com.termux.app.TermuxActivity"
android:label="@string/application_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:label="@string/application_name"
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|uiMode|keyboard|keyboardHidden|navigation"
android:launchMode="singleTask"
android:resizeableActivity="true"
android:windowSoftInputMode="adjustResize|stateAlwaysVisible" >
android:windowSoftInputMode="adjustResize|stateAlwaysVisible" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -78,6 +85,7 @@
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" />
<data android:mimeType="application/*log*" />
<data android:mimeType="application/json" />
<data android:mimeType="application/*xml*" />
<data android:mimeType="application/*latex*" />
@@ -112,6 +120,15 @@
android:name="com.termux.app.TermuxService"
android:exported="false" />
<service
android:name=".app.RunCommandService"
android:exported="true"
android:permission="com.termux.permission.RUN_COMMAND" >
<intent-filter>
<action android:name="com.termux.RUN_COMMAND" />
</intent-filter>
</service>
<receiver android:name=".app.TermuxOpenReceiver" />
<provider android:authorities="com.termux.files"
@@ -119,8 +136,8 @@
android:exported="true"
android:grantUriPermissions="true"
android:name="com.termux.app.TermuxOpenReceiver$ContentProvider" />
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
<meta-data android:name="com.samsung.android.multidisplay.keep_process_alive" android:value="true"/>
</application>
</manifest>

View File

@@ -0,0 +1,5 @@
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := libtermux-bootstrap
LOCAL_SRC_FILES := termux-bootstrap-zip.S termux-bootstrap.c
include $(BUILD_SHARED_LIBRARY)

View File

@@ -0,0 +1,18 @@
.global blob
.global blob_size
.section .rodata
blob:
#if defined __i686__
.incbin "bootstrap-i686.zip"
#elif defined __x86_64__
.incbin "bootstrap-x86_64.zip"
#elif defined __aarch64__
.incbin "bootstrap-aarch64.zip"
#elif defined __arm__
.incbin "bootstrap-arm.zip"
#else
# error Unsupported arch
#endif
1:
blob_size:
.int 1b - blob

View File

@@ -0,0 +1,11 @@
#include <jni.h>
extern jbyte blob[];
extern int blob_size;
JNIEXPORT jbyteArray JNICALL Java_com_termux_app_TermuxInstaller_getZip(JNIEnv *env, __attribute__((__unused__)) jobject This)
{
jbyteArray ret = (*env)->NewByteArray(env, blob_size);
(*env)->SetByteArrayRegion(env, ret, 0, blob_size, blob);
return ret;
}

View File

@@ -1,5 +1,9 @@
package com.termux.app;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import java.io.BufferedReader;
@@ -24,7 +28,11 @@ public final class BackgroundJob {
final Process mProcess;
public BackgroundJob(String cwd, String fileToExecute, final String[] args, final TermuxService service) {
public BackgroundJob(String cwd, String fileToExecute, final String[] args, final TermuxService service){
this(cwd, fileToExecute, args, service, null);
}
public BackgroundJob(String cwd, String fileToExecute, final String[] args, final TermuxService service, PendingIntent pendingIntent) {
String[] env = buildEnvironment(false, cwd);
if (cwd == null) cwd = TermuxService.HOME_PATH;
@@ -43,6 +51,28 @@ public final class BackgroundJob {
mProcess = process;
final int pid = getPid(mProcess);
final Bundle result = new Bundle();
final StringBuilder outResult = new StringBuilder();
final StringBuilder errResult = new StringBuilder();
Thread errThread = new Thread() {
@Override
public void run() {
InputStream stderr = mProcess.getErrorStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(stderr, StandardCharsets.UTF_8));
String line;
try {
// FIXME: Long lines.
while ((line = reader.readLine()) != null) {
errResult.append(line).append('\n');
Log.i(LOG_TAG, "[" + pid + "] stderr: " + line);
}
} catch (IOException e) {
// Ignore.
}
}
};
errThread.start();
new Thread() {
@Override
@@ -50,11 +80,13 @@ public final class BackgroundJob {
Log.i(LOG_TAG, "[" + pid + "] starting: " + processDescription);
InputStream stdout = mProcess.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(stdout, StandardCharsets.UTF_8));
String line;
try {
// FIXME: Long lines.
while ((line = reader.readLine()) != null) {
Log.i(LOG_TAG, "[" + pid + "] stdout: " + line);
outResult.append(line).append('\n');
}
} catch (IOException e) {
Log.e(LOG_TAG, "Error reading output", e);
@@ -68,29 +100,35 @@ public final class BackgroundJob {
} else {
Log.w(LOG_TAG, "[" + pid + "] exited with code: " + exitCode);
}
result.putString("stdout", outResult.toString());
result.putInt("exitCode", exitCode);
errThread.join();
result.putString("stderr", errResult.toString());
Intent data = new Intent();
data.putExtra("result", result);
if(pendingIntent != null) {
try {
pendingIntent.send(service.getApplicationContext(), Activity.RESULT_OK, data);
} catch (PendingIntent.CanceledException e) {
// The caller doesn't want the result? That's fine, just ignore
}
}
} catch (InterruptedException e) {
// Ignore.
// Ignore
}
}
}.start();
}
new Thread() {
@Override
public void run() {
InputStream stderr = mProcess.getErrorStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(stderr, StandardCharsets.UTF_8));
String line;
try {
// FIXME: Long lines.
while ((line = reader.readLine()) != null) {
Log.i(LOG_TAG, "[" + pid + "] stderr: " + line);
}
} catch (IOException e) {
// Ignore.
}
}
};
private static void addToEnvIfPresent(List<String> environment, String name) {
String value = System.getenv(name);
if (value != null) {
environment.add(name + "=" + value);
}
}
static String[] buildEnvironment(boolean failSafe, String cwd) {
@@ -103,17 +141,15 @@ public final class BackgroundJob {
environment.add("TERM=xterm-256color");
environment.add("HOME=" + TermuxService.HOME_PATH);
environment.add("PREFIX=" + TermuxService.PREFIX_PATH);
environment.add("BOOTCLASSPATH" + System.getenv("BOOTCLASSPATH"));
environment.add("BOOTCLASSPATH=" + System.getenv("BOOTCLASSPATH"));
environment.add("ANDROID_ROOT=" + System.getenv("ANDROID_ROOT"));
environment.add("ANDROID_DATA=" + System.getenv("ANDROID_DATA"));
// EXTERNAL_STORAGE is needed for /system/bin/am to work on at least
// Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3.
environment.add("EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE"));
String androidRuntimeRoot = System.getenv("ANDROID_RUNTIME_ROOT");
// ANDROID_RUNTIME_ROOT is required for `am` to run on Android Q
if (androidRuntimeRoot != null) {
environment.add("ANDROID_RUNTIME_ROOT=" + androidRuntimeRoot);
}
// ANDROID_RUNTIME_ROOT and ANDROID_TZDATA_ROOT are required for `am` to run on Android Q
addToEnvIfPresent(environment, "ANDROID_RUNTIME_ROOT");
addToEnvIfPresent(environment, "ANDROID_TZDATA_ROOT");
if (failSafe) {
// Keep the default path so that system binaries can be used in the failsafe session.
environment.add("PATH= " + System.getenv("PATH"));
@@ -134,14 +170,14 @@ public final class BackgroundJob {
try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(TermuxService.PREFIX_PATH + "/etc/apt/sources.list")))) {
String line;
while ((line = in.readLine()) != null) {
if (!line.startsWith("#") && line.contains("https://dl.bintray.com/termux/termux-packages-24")) {
return false;
if (!line.startsWith("#") && line.contains("//termux.net stable")) {
return true;
}
}
} catch (IOException e) {
Log.e(LOG_TAG, "Error trying to read sources.list", e);
}
return true;
return false;
}
public static int getPid(Process p) {

View File

@@ -10,7 +10,7 @@ public class BellUtil {
private static BellUtil instance = null;
private static final Object lock = new Object();
public static BellUtil with(Context context) {
public static BellUtil getInstance(Context context) {
if (instance == null) {
synchronized (lock) {
if (instance == null) {

View File

@@ -0,0 +1,338 @@
package com.termux.app;
import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
public class ExtraKeysInfos {
/**
* Matrix of buttons displayed
*/
private ExtraKeyButton[][] buttons;
/**
* This corresponds to one of the CharMapDisplay below
*/
private String style = "default";
public ExtraKeysInfos(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:
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);
}
}
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 String key;
/**
* If the key is a macro, i.e. a sequence of keys separated by space.
*/
private boolean macro;
/**
* The text that will be shown on the button.
*/
private String display;
/**
* The information of the popup (triggered by swipe up).
*/
@Nullable
private ExtraKeyButton popup = null;
public ExtraKeyButton(ExtraKeysInfos.CharDisplayMap charDisplayMap, JSONObject config) throws JSONException {
this(charDisplayMap, config, null);
}
public ExtraKeyButton(ExtraKeysInfos.CharDisplayMap charDisplayMap, JSONObject config, 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] = ExtraKeysInfos.replaceAlias(keys[i]);
}
this.key = String.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,7 +1,9 @@
package com.termux.app;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.provider.Settings;
import android.util.AttributeSet;
import java.util.concurrent.Executors;
@@ -12,19 +14,22 @@ import java.util.Map;
import java.util.HashMap;
import java.util.Arrays;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.GridLayout;
import android.widget.PopupWindow;
import android.widget.ToggleButton;
import com.termux.R;
import com.termux.terminal.TerminalSession;
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.
@@ -34,31 +39,14 @@ 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 = 0x7FFFFFFF;
private static final int BUTTON_PRESSED_COLOR = 0xFF7F7F7F;
public ExtraKeysView(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* 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 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);
@@ -73,46 +61,92 @@ public final class ExtraKeysView extends GridLayout {
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);
}};
static void sendKey(View view, String keyName) {
private void sendKey(View view, String keyName, boolean forceCtrlDown, boolean forceLeftAltDown) {
TerminalView terminalView = view.findViewById(R.id.terminal_view);
if (keyCodesForString.containsKey(keyName)) {
if ("KEYBOARD".equals(keyName)) {
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(0, 0);
} else if ("DRAWER".equals(keyName)) {
DrawerLayout drawer = view.findViewById(R.id.drawer_layout);
drawer.openDrawer(Gravity.LEFT);
} else if (keyCodesForString.containsKey(keyName)) {
int keyCode = keyCodesForString.get(keyName);
terminalView.onKeyDown(keyCode, new KeyEvent(KeyEvent.ACTION_UP, keyCode));
// view.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
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
TerminalSession session = terminalView.getCurrentSession();
if (session != null && keyName.length() > 0)
session.write(keyName);
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;
ToggleButton button = null;
}
private Map<SpecialButton, SpecialButtonState> specialButtons = new HashMap<SpecialButton, SpecialButtonState>() {{
put(SpecialButton.CTRL, new SpecialButtonState());
put(SpecialButton.ALT, new SpecialButtonState());
put(SpecialButton.FN, new SpecialButtonState());
}};
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)
return false;
@@ -153,145 +187,21 @@ public final class ExtraKeysView extends GridLayout {
popupWindow.setFocusable(false);
popupWindow.showAsDropDown(view, 0, -2 * height);
}
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
}};
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
}};
/**
* Keys are displayed in a natural looking way, like "→" for "RIGHT" or "↲" for ENTER
*/
public static final CharDisplayMap defaultCharDisplay = new CharDisplayMap() {{
putAll(classicArrowsDisplay);
putAll(wellKnownCharactersDisplay);
putAll(nicerLookingDisplay);
// all other characters are displayed as themselves
}};
public static final CharDisplayMap lotsOfArrowsCharDisplay = new CharDisplayMap() {{
putAll(classicArrowsDisplay);
putAll(wellKnownCharactersDisplay);
putAll(lessKnownCharactersDisplay); // NEW
putAll(nicerLookingDisplay);
}};
public static final CharDisplayMap arrowsOnlyCharDisplay = new CharDisplayMap() {{
putAll(classicArrowsDisplay);
// putAll(wellKnownCharactersDisplay); // REMOVED
// putAll(lessKnownCharactersDisplay); // REMOVED
putAll(nicerLookingDisplay);
}};
public 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 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", "'");
}};
/**
* Applies the 'controlCharsAliases' mapping to all the strings in *buttons*
* Modifies the array, doesn't return a new one.
*/
void replaceAliases(String[][] buttons) {
for(int i = 0; i < buttons.length; i++)
for(int j = 0; j < buttons[i].length; j++)
buttons[i][j] = controlCharsAliases.get(buttons[i][j], buttons[i][j]);
}
/**
* General util function to compute the longest column length in a matrix.
*/
static int maximumLength(String[][] matrix) {
static int maximumLength(Object[][] matrix) {
int m = 0;
for (String[] aMatrix : matrix) m = Math.max(m, aMatrix.length);
for (Object[] row : matrix)
m = Math.max(m, row.length);
return m;
}
/**
* Reload the view given parameters in termux.properties
*
* @param buttons matrix of String as defined in termux.properties extrakeys
* @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
@@ -303,49 +213,61 @@ public final class ExtraKeysView extends GridLayout {
* "" will input a "" character
* "-_-" will input the string "-_-"
*/
void reload(String[][] buttons, CharDisplayMap charDisplayMap) {
@SuppressLint("ClickableViewAccessibility")
void reload(ExtraKeysInfos infos) {
if(infos == null)
return;
for(SpecialButtonState state : specialButtons.values())
state.button = null;
removeAllViews();
replaceAliases(buttons); // modifies the array
final int rows = buttons.length;
final int cols = maximumLength(buttons);
ExtraKeyButton[][] buttons = infos.getMatrix();
setRowCount(rows);
setColumnCount(cols);
setRowCount(buttons.length);
setColumnCount(maximumLength(buttons));
for (int row = 0; row < rows; row++) {
for (int row = 0; row < buttons.length; row++) {
for (int col = 0; col < buttons[row].length; col++) {
final String buttonText = buttons[row][col];
final ExtraKeyButton buttonInfo = buttons[row][col];
Button button;
if(Arrays.asList("CTRL", "ALT", "FN").contains(buttonText)) {
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonText)); // for valueOf: https://stackoverflow.com/a/604426/1980630
if(Arrays.asList("CTRL", "ALT", "FN").contains(buttonInfo.getKey())) {
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonInfo.getKey())); // for valueOf: https://stackoverflow.com/a/604426/1980630
state.isOn = true;
button = state.button = new ToggleButton(getContext(), null, android.R.attr.buttonBarButtonStyle);
button.setClickable(true);
} else {
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
}
final String displayedText = charDisplayMap.get(buttonText, buttonText);
button.setText(displayedText);
button.setText(buttonInfo.getDisplay());
button.setTextColor(TEXT_COLOR);
button.setPadding(0, 0, 0, 0);
final Button finalButton = button;
button.setOnClickListener(v -> {
finalButton.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
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(Arrays.asList("CTRL", "ALT", "FN").contains(buttonText)) {
if (Arrays.asList("CTRL", "ALT", "FN").contains(buttonInfo.getKey())) {
ToggleButton self = (ToggleButton) finalButton;
self.setChecked(self.isChecked());
self.setTextColor(self.isChecked() ? INTERESTING_COLOR : TEXT_COLOR);
} else {
sendKey(root, buttonText);
sendKey(root, buttonInfo);
}
});
@@ -355,22 +277,26 @@ public final class ExtraKeysView extends GridLayout {
case MotionEvent.ACTION_DOWN:
longPressCount = 0;
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
if (Arrays.asList("UP", "DOWN", "LEFT", "RIGHT").contains(buttonText)) {
if (Arrays.asList("UP", "DOWN", "LEFT", "RIGHT", "BKSP", "DEL").contains(buttonInfo.getKey())) {
// autorepeat
scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
scheduledExecutor.scheduleWithFixedDelay(() -> {
longPressCount++;
sendKey(root, buttonText);
sendKey(root, buttonInfo);
}, 400, 80, TimeUnit.MILLISECONDS);
}
return true;
case MotionEvent.ACTION_MOVE:
// These two keys have a Move-Up button appearing
if (Arrays.asList("/", "-").contains(buttonText)) {
if (buttonInfo.getPopup() != null) {
if (popupWindow == null && event.getY() < 0) {
if (scheduledExecutor != null) {
scheduledExecutor.shutdownNow();
scheduledExecutor = null;
}
v.setBackgroundColor(BUTTON_COLOR);
String text = "-".equals(buttonText) ? "|" : "\\";
popup(v, text);
String extraButtonDisplayedText = buttonInfo.getPopup().getDisplay();
popup(v, extraButtonDisplayedText);
}
if (popupWindow != null && event.getY() > 0) {
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
@@ -380,19 +306,27 @@ public final class ExtraKeysView extends GridLayout {
}
return true;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
v.setBackgroundColor(BUTTON_COLOR);
if (scheduledExecutor != null) {
scheduledExecutor.shutdownNow();
scheduledExecutor = null;
}
if (longPressCount == 0) {
if (popupWindow != null && Arrays.asList("/", "-").contains(buttonText)) {
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;
sendKey(root, "-".equals(buttonText) ? "|" : "\\");
if (buttonInfo.getPopup() != null) {
sendKey(root, buttonInfo.getPopup());
}
} else {
v.performClick();
}
@@ -406,11 +340,7 @@ public final class ExtraKeysView extends GridLayout {
LayoutParams param = new GridLayout.LayoutParams();
param.width = 0;
if(Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) { // special handle api 21
param.height = (int)(37.5 * getResources().getDisplayMetrics().density + 0.5); // 37.5 equal to R.id.viewpager layout_height / rows in DP
} else {
param.height = 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);

View File

@@ -0,0 +1,86 @@
package com.termux.app;
import android.app.Service;
import android.content.Intent;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Properties;
/**
* When allow-external-apps property is set to "true", Termux is able to process execute intents
* sent by third-party applications.
*
* Third-party program must declare com.termux.permission.RUN_COMMAND permission and it should be
* granted by user.
*
* Sample code to run command "top":
* Intent intent = new Intent();
* intent.setClassName("com.termux", "com.termux.app.RunCommandService");
* intent.setAction("com.termux.RUN_COMMAND");
* intent.putExtra("com.termux.RUN_COMMAND_PATH", "/data/data/com.termux/files/usr/bin/top");
* startService(intent);
*/
public class RunCommandService extends Service {
public static final String RUN_COMMAND_ACTION = "com.termux.RUN_COMMAND";
public static final String RUN_COMMAND_PATH = "com.termux.RUN_COMMAND_PATH";
public static final String RUN_COMMAND_ARGUMENTS = "com.termux.RUN_COMMAND_ARGUMENTS";
public static final String RUN_COMMAND_WORKDIR = "com.termux.RUN_COMMAND_WORKDIR";
class LocalBinder extends Binder {
public final RunCommandService service = RunCommandService.this;
}
private final IBinder mBinder = new RunCommandService.LocalBinder();
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
public int onStartCommand(Intent intent, int flags, int startId) {
if (allowExternalApps() && RUN_COMMAND_ACTION.equals(intent.getAction())) {
Uri programUri = new Uri.Builder().scheme("com.termux.file").path(intent.getStringExtra(RUN_COMMAND_PATH)).build();
Intent execIntent = new Intent(TermuxService.ACTION_EXECUTE, programUri);
execIntent.setClass(this, TermuxService.class);
execIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, intent.getStringExtra(RUN_COMMAND_ARGUMENTS));
execIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, intent.getStringExtra(RUN_COMMAND_WORKDIR));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
this.startForegroundService(execIntent);
} else {
this.startService(execIntent);
}
}
return Service.START_NOT_STICKY;
}
private boolean allowExternalApps() {
File propsFile = new File(TermuxService.HOME_PATH + "/.termux/termux.properties");
if (!propsFile.exists())
propsFile = new File(TermuxService.HOME_PATH + "/.config/termux/termux.properties");
Properties props = new Properties();
try {
if (propsFile.isFile() && propsFile.canRead()) {
try (FileInputStream in = new FileInputStream(propsFile)) {
props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
}
}
} catch (Exception e) {
Log.e("termux", "Error loading props", e);
}
return props.getProperty("allow-external-apps", "false").equals("true");
}
}

View File

@@ -24,7 +24,6 @@ import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Vibrator;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
@@ -128,6 +127,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
*/
boolean mIsVisible;
boolean mIsUsingBlackUI;
final SoundPool mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
@@ -147,7 +148,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
mSettings.reloadFromProperties(TermuxActivity.this);
if (mExtraKeysView != null) {
mExtraKeysView.reload(mSettings.mExtraKeys, ExtraKeysView.defaultCharDisplay);
mExtraKeysView.reload(mSettings.mExtraKeys);
}
}
}
@@ -187,28 +188,35 @@ public final class TermuxActivity extends Activity implements ServiceConnection
}
/** For processes to access shared internal storage (/sdcard) we need this permission. */
@TargetApi(Build.VERSION_CODES.M)
public boolean ensureStoragePermissionGranted() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
return true;
} else {
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUESTCODE_PERMISSION_STORAGE);
return false;
}
} else {
// Always granted before Android 6.0.
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
return true;
} else {
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUESTCODE_PERMISSION_STORAGE);
return false;
}
}
@Override
public void onCreate(Bundle bundle) {
mSettings = new TermuxPreferences(this);
mIsUsingBlackUI = mSettings.isUsingBlackUI();
if (mIsUsingBlackUI) {
this.setTheme(R.style.Theme_Termux_Black);
} else {
this.setTheme(R.style.Theme_Termux);
}
super.onCreate(bundle);
mSettings = new TermuxPreferences(this);
setContentView(R.layout.drawer_layout);
if (mIsUsingBlackUI) {
findViewById(R.id.left_drawer).setBackgroundColor(
getResources().getColor(android.R.color.background_dark)
);
}
mTerminalView = findViewById(R.id.terminal_view);
mTerminalView.setOnKeyListener(new TermuxViewClient(this));
@@ -221,7 +229,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
ViewGroup.LayoutParams layoutParams = viewPager.getLayoutParams();
layoutParams.height = layoutParams.height * mSettings.mExtraKeys.length;
layoutParams.height = layoutParams.height * (mSettings.mExtraKeys == null ? 0 : mSettings.mExtraKeys.getMatrix().length);
viewPager.setLayoutParams(layoutParams);
viewPager.setAdapter(new PagerAdapter() {
@@ -242,7 +250,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
View layout;
if (position == 0) {
layout = mExtraKeysView = (ExtraKeysView) inflater.inflate(R.layout.extra_keys_main, collection, false);
mExtraKeysView.reload(mSettings.mExtraKeys, ExtraKeysView.defaultCharDisplay);
mExtraKeysView.reload(mSettings.mExtraKeys);
} else {
layout = inflater.inflate(R.layout.extra_keys_right, collection, false);
final EditText editText = layout.findViewById(R.id.text_input);
@@ -402,7 +410,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
break;
case TermuxPreferences.BELL_VIBRATE:
BellUtil.with(TermuxActivity.this).doBell();
BellUtil.getInstance(TermuxActivity.this).doBell();
break;
case TermuxPreferences.BELL_IGNORE:
// Ignore the bell character.
@@ -435,7 +443,11 @@ public final class TermuxActivity extends Activity implements ServiceConnection
boolean sessionRunning = sessionAtRow.isRunning();
TextView firstLineView = row.findViewById(R.id.row_line);
if (mIsUsingBlackUI) {
firstLineView.setBackground(
getResources().getDrawable(R.drawable.selected_session_background_black)
);
}
String name = sessionAtRow.mSessionName;
String sessionTitle = sessionAtRow.getTitle();
@@ -455,7 +467,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
} else {
firstLineView.setPaintFlags(firstLineView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
}
int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? Color.BLACK : Color.RED;
int defaultColor = mIsUsingBlackUI ? Color.WHITE : Color.BLACK;
int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? defaultColor : Color.RED;
firstLineView.setTextColor(color);
return row;
}
@@ -590,8 +603,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
new AlertDialog.Builder(this).setTitle(R.string.max_terminals_reached_title).setMessage(R.string.max_terminals_reached_message)
.setPositiveButton(android.R.string.ok, null).show();
} else {
String executablePath = (failSafe ? "/system/bin/sh" : null);
TerminalSession newSession = mTermService.createTermSession(executablePath, null, null, failSafe);
TerminalSession currentSession = getCurrentTermSession();
String workingDirectory = (currentSession == null) ? null : currentSession.getCwd();
TerminalSession newSession = mTermService.createTermSession(null, null, workingDirectory, failSafe);
if (sessionName != null) {
newSession.mSessionName = sessionName;
}
@@ -656,24 +670,94 @@ public final class TermuxActivity extends Activity implements ServiceConnection
}
static LinkedHashSet<CharSequence> extractUrls(String text) {
// Pattern for recognizing a URL, based off RFC 3986
// http://stackoverflow.com/questions/5713558/detect-and-extract-url-from-a-string
StringBuilder regex_sb = new StringBuilder();
regex_sb.append("("); // Begin first matching group.
regex_sb.append("(?:"); // Begin scheme group.
regex_sb.append("dav|"); // The DAV proto.
regex_sb.append("dict|"); // The DICT proto.
regex_sb.append("dns|"); // The DNS proto.
regex_sb.append("file|"); // File path.
regex_sb.append("finger|"); // The Finger proto.
regex_sb.append("ftp(?:s?)|"); // The FTP proto.
regex_sb.append("git|"); // The Git proto.
regex_sb.append("gopher|"); // The Gopher proto.
regex_sb.append("http(?:s?)|"); // The HTTP proto.
regex_sb.append("imap(?:s?)|"); // The IMAP proto.
regex_sb.append("irc(?:[6s]?)|"); // The IRC proto.
regex_sb.append("ip[fn]s|"); // The IPFS proto.
regex_sb.append("ldap(?:s?)|"); // The LDAP proto.
regex_sb.append("pop3(?:s?)|"); // The POP3 proto.
regex_sb.append("redis(?:s?)|"); // The Redis proto.
regex_sb.append("rsync|"); // The Rsync proto.
regex_sb.append("rtsp(?:[su]?)|"); // The RTSP proto.
regex_sb.append("sftp|"); // The SFTP proto.
regex_sb.append("smb(?:s?)|"); // The SAMBA proto.
regex_sb.append("smtp(?:s?)|"); // The SMTP proto.
regex_sb.append("svn(?:(?:\\+ssh)?)|"); // The Subversion proto.
regex_sb.append("tcp|"); // The TCP proto.
regex_sb.append("telnet|"); // The Telnet proto.
regex_sb.append("tftp|"); // The TFTP proto.
regex_sb.append("udp|"); // The UDP proto.
regex_sb.append("vnc|"); // The VNC proto.
regex_sb.append("ws(?:s?)"); // The Websocket proto.
regex_sb.append(")://"); // End scheme group.
regex_sb.append(")"); // End first matching group.
// Begin second matching group.
regex_sb.append("(");
// User name and/or password in format 'user:pass@'.
regex_sb.append("(?:\\S+(?::\\S*)?@)?");
// Begin host group.
regex_sb.append("(?:");
// IP address (from http://www.regular-expressions.info/examples.html).
regex_sb.append("(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|");
// Host name or domain.
regex_sb.append("(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)(?:(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))?|");
// Just path. Used in case of 'file://' scheme.
regex_sb.append("/(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)");
// End host group.
regex_sb.append(")");
// Port number.
regex_sb.append("(?::\\d{1,5})?");
// Resource path with optional query string.
regex_sb.append("(?:/[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
// Fragment.
regex_sb.append("(?:#[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
// End second matching group.
regex_sb.append(")");
final Pattern urlPattern = Pattern.compile(
"(?:^|[\\W])((ht|f)tp(s?)://|www\\.)" + "(([\\w\\-]+\\.)+?([\\w\\-.~]+/?)*" + "[\\p{Alnum}.,%_=?&#\\-+()\\[\\]*$~@!:/{};']*)",
regex_sb.toString(),
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
LinkedHashSet<CharSequence> urlSet = new LinkedHashSet<>();
Matcher matcher = urlPattern.matcher(text);
while (matcher.find()) {
int matchStart = matcher.start(1);
int matchEnd = matcher.end();
String url = text.substring(matchStart, matchEnd);
urlSet.add(url);
}
return urlSet;
}
void showUrlSelection() {
String text = getCurrentTermSession().getEmulator().getScreen().getTranscriptText();
String text = getCurrentTermSession().getEmulator().getScreen().getTranscriptTextWithFullLinesJoined();
LinkedHashSet<CharSequence> urlSet = extractUrls(text);
if (urlSet.isEmpty()) {
new AlertDialog.Builder(this).setMessage(R.string.select_url_no_found).show();
@@ -723,7 +807,18 @@ public final class TermuxActivity extends Activity implements ServiceConnection
if (session != null) {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, session.getEmulator().getScreen().getTranscriptText().trim());
String transcriptText = session.getEmulator().getScreen().getTranscriptTextWithoutJoinedLines().trim();
// See https://github.com/termux/termux-app/issues/1166.
final int MAX_LENGTH = 100_000;
if (transcriptText.length() > MAX_LENGTH) {
int cutOffIndex = transcriptText.length() - MAX_LENGTH;
int nextNewlineIndex = transcriptText.indexOf('\n', cutOffIndex);
if (nextNewlineIndex != -1 && nextNewlineIndex != transcriptText.length() - 1) {
cutOffIndex = nextNewlineIndex + 1;
}
transcriptText = transcriptText.substring(cutOffIndex).trim();
}
intent.putExtra(Intent.EXTRA_TEXT, transcriptText);
intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_transcript_title));
startActivity(Intent.createChooser(intent, getString(R.string.share_transcript_chooser_title)));
}

View File

@@ -4,7 +4,6 @@ import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.Build;
import android.os.Environment;
import android.os.UserManager;
import android.system.Os;
@@ -16,14 +15,12 @@ import com.termux.R;
import com.termux.terminal.EmulatorDebug;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@@ -38,7 +35,7 @@ import java.util.zip.ZipInputStream;
* <p/>
* (3) A staging folder, $STAGING_PREFIX, is {@link #deleteFolder(File)} if left over from broken installation below.
* <p/>
* (4) The architecture is determined and an appropriate bootstrap zip url is determined in {@link #determineZipUrl()}.
* (4) The zip file is loaded from a shared library.
* <p/>
* (5) The zip, containing entries relative to the $PREFIX, is is downloaded and extracted by a zip input stream
* continuously encountering zip file entries:
@@ -82,8 +79,8 @@ final class TermuxInstaller {
final byte[] buffer = new byte[8096];
final List<Pair<String, String>> symlinks = new ArrayList<>(50);
final URL zipUrl = determineZipUrl();
try (ZipInputStream zipInput = new ZipInputStream(zipUrl.openStream())) {
final byte[] zipBytes = loadZipBytes();
try (ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
ZipEntry zipEntry;
while ((zipEntry = zipInput.getNextEntry()) != null) {
if (zipEntry.getName().equals("SYMLINKS.txt")) {
@@ -167,34 +164,13 @@ final class TermuxInstaller {
}
}
/** Get bootstrap zip url for this systems cpu architecture. */
private static URL determineZipUrl() throws MalformedURLException {
String archName = determineTermuxArchName();
String url = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
? "https://termux.org/bootstrap-" + archName + ".zip"
: "https://termux.net/bootstrap/bootstrap-" + archName + ".zip";
return new URL(url);
public static byte[] loadZipBytes() {
// Only load the shared library when necessary to save memory usage.
System.loadLibrary("termux-bootstrap");
return getZip();
}
private static String determineTermuxArchName() {
// Note that we cannot use System.getProperty("os.arch") since that may give e.g. "aarch64"
// while a 64-bit runtime may not be installed (like on the Samsung Galaxy S5 Neo).
// Instead we search through the supported abi:s on the device, see:
// http://developer.android.com/ndk/guides/abis.html
// Note that we search for abi:s in preferred order (the ordering of the
// Build.SUPPORTED_ABIS list) to avoid e.g. installing arm on an x86 system where arm
// emulation is available.
for (String androidArch : Build.SUPPORTED_ABIS) {
switch (androidArch) {
case "arm64-v8a": return "aarch64";
case "armeabi-v7a": return "arm";
case "x86_64": return "x86_64";
case "x86": return "i686";
}
}
throw new RuntimeException("Unable to determine arch from Build.SUPPORTED_ABIS = " +
Arrays.toString(Build.SUPPORTED_ABIS));
}
public static native byte[] getZip();
/** Delete a folder and all its content or throw. Don't follow symlinks. */
static void deleteFolder(File fileOrDirectory) throws IOException {

View File

@@ -2,6 +2,7 @@ package com.termux.app;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.preference.PreferenceManager;
import android.util.Log;
import android.util.TypedValue;
@@ -9,6 +10,7 @@ import android.widget.Toast;
import com.termux.terminal.TerminalSession;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileInputStream;
@@ -18,7 +20,10 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import androidx.annotation.IntDef;
@@ -58,6 +63,7 @@ final class TermuxPreferences {
private static final String CURRENT_SESSION_KEY = "current_session";
private static final String SCREEN_ALWAYS_ON_KEY = "screen_always_on";
private boolean mUseDarkUI;
private boolean mScreenAlwaysOn;
private int mFontSize;
@@ -65,9 +71,10 @@ final class TermuxPreferences {
int mBellBehaviour = BELL_VIBRATE;
boolean mBackIsEscape;
boolean mDisableVolumeVirtualKeys;
boolean mShowExtraKeys;
String[][] mExtraKeys;
ExtraKeysInfos mExtraKeys;
final List<KeyboardShortcut> shortcuts = new ArrayList<>();
@@ -126,6 +133,10 @@ final class TermuxPreferences {
return mScreenAlwaysOn;
}
boolean isUsingBlackUI() {
return mUseDarkUI;
}
void setScreenAlwaysOn(Context context, boolean newValue) {
mScreenAlwaysOn = newValue;
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SCREEN_ALWAYS_ON_KEY, newValue).apply();
@@ -143,7 +154,7 @@ final class TermuxPreferences {
}
return null;
}
void reloadFromProperties(Context context) {
File propsFile = new File(TermuxService.HOME_PATH + "/.termux/termux.properties");
if (!propsFile.exists())
@@ -156,8 +167,8 @@ final class TermuxPreferences {
props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
}
}
} catch (IOException e) {
Toast.makeText(context, "Could not open properties file termux.properties.", Toast.LENGTH_LONG).show();
} catch (Exception e) {
Toast.makeText(context, "Could not open properties file termux.properties: " + e.getMessage(), Toast.LENGTH_LONG).show();
Log.e("termux", "Error loading props", e);
}
@@ -173,24 +184,39 @@ final class TermuxPreferences {
break;
}
try {
JSONArray arr = new JSONArray(props.getProperty("extra-keys", "[['ESC', 'TAB', 'CTRL', 'ALT', '-', 'DOWN', 'UP']]"));
switch (props.getProperty("use-black-ui", "").toLowerCase()) {
case "true":
mUseDarkUI = true;
break;
case "false":
mUseDarkUI = false;
break;
default:
int nightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
mUseDarkUI = nightMode == Configuration.UI_MODE_NIGHT_YES;
}
mExtraKeys = new String[arr.length()][];
for (int i = 0; i < arr.length(); i++) {
JSONArray line = arr.getJSONArray(i);
mExtraKeys[i] = new String[line.length()];
for (int j = 0; j < line.length(); j++) {
mExtraKeys[i][j] = line.getString(j);
}
}
String defaultExtraKeys = "[[ESC, TAB, CTRL, ALT, {key: '-', popup: '|'}, DOWN, UP]]";
try {
String extrakeyProp = props.getProperty("extra-keys", defaultExtraKeys);
String extraKeysStyle = props.getProperty("extra-keys-style", "default");
mExtraKeys = new ExtraKeysInfos(extrakeyProp, extraKeysStyle);
} catch (JSONException e) {
Toast.makeText(context, "Could not load the extra-keys property from the config: " + e.toString(), Toast.LENGTH_LONG).show();
Log.e("termux", "Error loading props", e);
mExtraKeys = new String[0][];
try {
mExtraKeys = new ExtraKeysInfos(defaultExtraKeys, "default");
} catch (JSONException e2) {
e2.printStackTrace();
Toast.makeText(context, "Can't create default extra keys", Toast.LENGTH_LONG).show();
mExtraKeys = null;
}
}
mBackIsEscape = "escape".equals(props.getProperty("back-key", "back"));
mDisableVolumeVirtualKeys = "volume".equals(props.getProperty("volume-keys", "virtual"));
shortcuts.clear();
parseAction("shortcut.create-session", SHORTCUT_ACTION_CREATE_SESSION, props);

View File

@@ -6,6 +6,7 @@ import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
@@ -16,6 +17,7 @@ import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager;
import android.provider.Settings;
import android.util.Log;
import android.widget.ArrayAdapter;
@@ -112,6 +114,20 @@ public final class TermuxService extends Service implements SessionChangedCallba
mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, EmulatorDebug.LOG_TAG);
mWifiLock.acquire();
String packageName = getPackageName();
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
Intent whitelist = new Intent();
whitelist.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
whitelist.setData(Uri.parse("package:" + packageName));
whitelist.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
startActivity(whitelist);
} catch (ActivityNotFoundException e) {
Log.e(EmulatorDebug.LOG_TAG, "Failed to call ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS", e);
}
}
updateNotification();
}
} else if (ACTION_UNLOCK_WAKE.equals(action)) {
@@ -132,11 +148,12 @@ public final class TermuxService extends Service implements SessionChangedCallba
String cwd = intent.getStringExtra(EXTRA_CURRENT_WORKING_DIRECTORY);
if (intent.getBooleanExtra(EXTRA_EXECUTE_IN_BACKGROUND, false)) {
BackgroundJob task = new BackgroundJob(cwd, executablePath, arguments, this);
BackgroundJob task = new BackgroundJob(cwd, executablePath, arguments, this, intent.getParcelableExtra("pendingIntent"));
mBackgroundTasks.add(task);
updateNotification();
} else {
TerminalSession newSession = createTermSession(executablePath, arguments, cwd, false);
boolean failsafe = intent.getBooleanExtra(TermuxActivity.TERMUX_FAILSAFE_SESSION_ACTION, false);
TerminalSession newSession = createTermSession(executablePath, arguments, cwd, failsafe);
// Transform executable path to session name, e.g. "/bin/do-something.sh" => "do something.sh".
if (executablePath != null) {
@@ -243,7 +260,7 @@ public final class TermuxService extends Service implements SessionChangedCallba
try {
TermuxInstaller.deleteFolder(termuxTmpDir.getCanonicalFile());
} catch (Exception e) {
Log.e(EmulatorDebug.LOG_TAG, "Error while removing directory " + termuxTmpDir.getAbsolutePath(), e);
Log.e(EmulatorDebug.LOG_TAG, "Error while removing file at " + termuxTmpDir.getAbsolutePath(), e);
}
termuxTmpDir.mkdirs();
@@ -271,11 +288,13 @@ public final class TermuxService extends Service implements SessionChangedCallba
boolean isLoginShell = false;
if (executablePath == null) {
for (String shellBinary : new String[]{"login", "bash", "zsh"}) {
File shellFile = new File(PREFIX_PATH + "/bin/" + shellBinary);
if (shellFile.canExecute()) {
executablePath = shellFile.getAbsolutePath();
break;
if (!failSafe) {
for (String shellBinary : new String[]{"login", "bash", "zsh"}) {
File shellFile = new File(PREFIX_PATH + "/bin/" + shellBinary);
if (shellFile.canExecute()) {
executablePath = shellFile.getAbsolutePath();
break;
}
}
}

View File

@@ -264,7 +264,9 @@ public final class TermuxViewClient implements TerminalViewClient {
/** Handle dedicated volume buttons as virtual keys if applicable. */
private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) {
InputDevice inputDevice = event.getDevice();
if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
if (mActivity.mSettings.mDisableVolumeVirtualKeys) {
return false;
} else if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
// Do not steal dedicated buttons from a full external keyboard.
return false;
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {

View File

@@ -71,7 +71,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
row.add(Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR));
row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(BASE_DIR));
row.add(Root.COLUMN_SUMMARY, null);
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH);
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD);
row.add(Root.COLUMN_TITLE, applicationName);
row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES);
row.add(Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.getFreeSpace());
@@ -117,6 +117,29 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
return true;
}
@Override
public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException {
File newFile = new File(parentDocumentId, displayName);
int noConflictId = 2;
while (newFile.exists()) {
newFile = new File(parentDocumentId, displayName + " (" + noConflictId++ + ")");
}
try {
boolean succeeded;
if (Document.MIME_TYPE_DIR.equals(mimeType)) {
succeeded = newFile.mkdir();
} else {
succeeded = newFile.createNewFile();
}
if (!succeeded) {
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
}
} catch (IOException e) {
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
}
return newFile.getPath();
}
@Override
public void deleteDocument(String documentId) throws FileNotFoundException {
File file = getFileForDocId(documentId);
@@ -169,6 +192,11 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
return result;
}
@Override
public boolean isChildDocument(String parentDocumentId, String documentId) {
return documentId.startsWith(parentDocumentId);
}
/**
* Get the document id given a file. This document id must be consistent across time as other
* applications may save the ID and use it to reference documents later.
@@ -220,10 +248,11 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
int flags = 0;
if (file.isDirectory()) {
if (file.isDirectory() && file.canWrite()) flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
if (file.canWrite()) flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
} else if (file.canWrite()) {
flags |= Document.FLAG_SUPPORTS_WRITE | Document.FLAG_SUPPORTS_DELETE;
flags |= Document.FLAG_SUPPORTS_WRITE;
}
if (file.getParentFile().canWrite()) flags |= Document.FLAG_SUPPORTS_DELETE;
final String displayName = file.getName();
final String mimeType = getMimeType(file);

View File

@@ -21,6 +21,7 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.regex.Pattern;
public class TermuxFileReceiverActivity extends Activity {
@@ -36,6 +37,11 @@ public class TermuxFileReceiverActivity extends Activity {
*/
boolean mFinishOnDismissNameDialog = true;
static boolean isSharedTextAnUrl(String sharedText) {
return Patterns.WEB_URL.matcher(sharedText).matches()
|| Pattern.matches("magnet:\\?xt=urn:btih:.*?", sharedText);
}
@Override
protected void onResume() {
super.onResume();
@@ -50,7 +56,7 @@ public class TermuxFileReceiverActivity extends Activity {
final Uri sharedUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
if (sharedText != null) {
if (Patterns.WEB_URL.matcher(sharedText).matches()) {
if (isSharedTextAnUrl(sharedText)) {
handleUrlAndFinish(sharedText);
} else {
String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="#E0E0E0" />
</shape>
</shape>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="#212325" />
</shape>

View File

@@ -1,33 +1,40 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="48dp"
android:width="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
android:height="108dp"
android:width="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!--
https://material.google.com/style/icons.html
-->
<!-- Screen border. -->
<path android:fillColor="#00000000"
android:strokeColor="#FFF"
android:strokeWidth="3"
android:pathData="M7,4
l34,0
q3 0,3 3
l0,34
q0 3, -3 3
l-34,0
q-3 0, -3-3
l0 -34
q0 -3, 3 -3"
/>
<!-- A circle as the icon border. -->
<path
android:fillColor="#00000000"
android:strokeColor="#FFF"
android:strokeWidth="3"
android:pathData="M18,54
A36,36 0 1,1 90,54
A36,36 0 1,1 18,54 Z" />
<!-- Block cursor. -->
<path android:fillColor="#FFF"
android:pathData="M14,14
l5,0
l0,10
l-5,0"
<!-- Keep in sync with ic_foreground.xml: -->
<path
android:fillColor="#FFFFFF"
android:pathData="M34,38
h6
l12,16
l-12,16
h-6
l12,-16
"
/>
<path
android:fillColor="#FFFFFF"
android:pathData="M56,66
h18
v4
h-18
"
/>
</vector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8" ?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_activated="true" android:drawable="@drawable/current_session_black"/>
<item android:state_activated="false" android:drawable="@drawable/session_ripple_black"/>
</selector>

View File

@@ -4,4 +4,4 @@
<item>
<color android:color="@android:color/white" />
</item>
</ripple>
</ripple>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@android:color/darker_gray" >
<item>
<color android:color="@android:color/background_dark" />
</item>
</ripple>

View File

@@ -2,6 +2,8 @@
<resources>
<string name="application_name">Termux</string>
<string name="shared_user_label">Termux user</string>
<string name="run_command_permission_label">Run commands in Termux environment</string>
<string name="run_command_permission_description">Allow third-party applications to execute arbitrary commands within Termux environment.</string>
<string name="new_session">New session</string>
<string name="new_session_failsafe">Failsafe</string>
<string name="toggle_soft_keyboard">Keyboard</string>
@@ -13,7 +15,7 @@
<string name="bootstrap_installer_body">Installing…</string>
<string name="bootstrap_error_title">Unable to install</string>
<string name="bootstrap_error_body">Termux was unable to install the bootstrap packages.\n\nCheck your network connection and try again.</string>
<string name="bootstrap_error_body">Termux was unable to install the bootstrap packages.</string>
<string name="bootstrap_error_abort">Abort</string>
<string name="bootstrap_error_try_again">Try again</string>
<string name="bootstrap_error_not_primary_user_message">Termux can only be installed on the primary user account.</string>

View File

@@ -1,8 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<!-- See https://developer.android.com/training/material/theme.html for how to customize the Material theme. -->
<!-- NOTE: Cannot use "Light." since it hides the terminal scrollbar on the default black background. -->
<style name="Theme.Termux" parent="@android:style/Theme.Material.Light.NoActionBar">
<item name="android:statusBarColor">#000000</item>
<item name="android:colorPrimary">#FF000000</item>
@@ -23,9 +21,29 @@
<item name="android:windowAllowEnterTransitionOverlap">true</item>
</style>
<style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">
<!-- Seen in buttons on alert dialog: -->
<style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">
<!-- Seen in buttons on alert dialog: -->
<item name="android:colorAccent">#212121</item>
</style>
<!-- See https://developer.android.com/training/material/theme.html for how to customize the Material theme. -->
<!-- NOTE: Cannot use "Light." since it hides the terminal scrollbar on the default black background. -->
<style name="Theme.Termux.Black" parent="@android:style/Theme.Material.NoActionBar">
<item name="android:statusBarColor">#000000</item>
<item name="android:colorPrimary">#FF000000</item>
<item name="android:windowBackground">@android:color/black</item>
<!-- Seen in buttons on left drawer: -->
<item name="android:colorAccent">#FDFDFD</item>
<!-- Avoid action mode toolbar pushing down terminal content when
selecting text on pre-6.0 (non-floating toolbar). -->
<item name="android:windowActionModeOverlay">true</item>
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowTranslucentNavigation">true</item>
<!-- https://developer.android.com/training/tv/start/start.html#transition-color -->
<item name="android:windowAllowReturnTransitionOverlap">true</item>
<item name="android:windowAllowEnterTransitionOverlap">true</item>
</style>
</resources>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<!-- See https://developer.android.com/training/backup/autosyncapi.html -->
<include domain="file" path="home/backup" />
</full-backup-content>

View File

@@ -1,25 +1,30 @@
package com.termux.app;
import junit.framework.TestCase;
import org.junit.Assert;
import org.junit.Test;
import java.util.Collections;
import java.util.LinkedHashSet;
public class TermuxActivityTest extends TestCase {
public class TermuxActivityTest {
private void assertUrlsAre(String text, String... urls) {
LinkedHashSet<String> expected = new LinkedHashSet<>();
Collections.addAll(expected, urls);
assertEquals(expected, TermuxActivity.extractUrls(text));
}
private void assertUrlsAre(String text, String... urls) {
LinkedHashSet<String> expected = new LinkedHashSet<>();
Collections.addAll(expected, urls);
Assert.assertEquals(expected, TermuxActivity.extractUrls(text));
}
public void testExtractUrls() {
assertUrlsAre("hello http://example.com world", "http://example.com");
@Test
public void testExtractUrls() {
assertUrlsAre("hello http://example.com world", "http://example.com");
assertUrlsAre("http://example.com\nhttp://another.com", "http://example.com", "http://another.com");
assertUrlsAre("http://example.com\nhttp://another.com", "http://example.com", "http://another.com");
assertUrlsAre("hello http://example.com world and http://more.example.com with secure https://more.example.com",
"http://example.com", "http://more.example.com", "https://more.example.com");
}
assertUrlsAre("hello http://example.com world and http://more.example.com with secure https://more.example.com",
"http://example.com", "http://more.example.com", "https://more.example.com");
assertUrlsAre("hello https://example.com/#bar https://example.com/foo#bar",
"https://example.com/#bar", "https://example.com/foo#bar");
}
}

View File

@@ -0,0 +1,32 @@
package com.termux.filepicker;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import java.util.ArrayList;
import java.util.List;
@RunWith(RobolectricTestRunner.class)
public class TermuxFileReceiverActivityTest {
@Test
public void testIsSharedTextAnUrl() {
List<String> validUrls = new ArrayList<>();
validUrls.add("http://example.com");
validUrls.add("https://example.com");
validUrls.add("https://example.com/path/parameter=foo");
validUrls.add("magnet:?xt=urn:btih:d540fc48eb12f2833163eed6421d449dd8f1ce1f&dn=Ubuntu+desktop+19.04+%2864bit%29&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=udp%3A%2F%2Ftracker.publicbt.com%3A80&tr=udp%3A%2F%2Ftracker.ccc.de%3A80");
for (String url : validUrls) {
Assert.assertTrue(TermuxFileReceiverActivity.isSharedTextAnUrl(url));
}
List<String> invalidUrls = new ArrayList<>();
invalidUrls.add("a test with example.com");
for (String url : invalidUrls) {
Assert.assertFalse(TermuxFileReceiverActivity.isSharedTextAnUrl(url));
}
}
}

View File

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

View File

@@ -13,3 +13,4 @@
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
org.gradle.jvmargs=-Xmx2048M
android.useAndroidX=true

Binary file not shown.

View File

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

51
gradlew vendored
View File

@@ -1,5 +1,21 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
@@ -28,7 +44,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m"'
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
@@ -109,8 +125,8 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
@@ -138,19 +154,19 @@ if $cygwin ; then
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
i=`expr $i + 1`
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
@@ -159,14 +175,9 @@ save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

21
gradlew.bat vendored
View File

@@ -1,3 +1,19 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@@ -13,8 +29,11 @@ if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m"
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

View File

@@ -18,6 +18,7 @@ ext {
android {
compileSdkVersion 28
ndkVersion '21.3.6528147'
defaultConfig {
minSdkVersion 21
@@ -60,7 +61,7 @@ tasks.withType(Test) {
}
dependencies {
testImplementation 'junit:junit:4.12'
testImplementation 'junit:junit:4.13'
}
apply from: '../scripts/bintray-publish.gradle'

View File

@@ -41,7 +41,23 @@ public final class TerminalBuffer {
return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows).trim();
}
public String getTranscriptTextWithoutJoinedLines() {
return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows, false).trim();
}
public String getTranscriptTextWithFullLinesJoined() {
return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows, true, true).trim();
}
public String getSelectedText(int selX1, int selY1, int selX2, int selY2) {
return getSelectedText(selX1, selY1, selX2, selY2, true);
}
public String getSelectedText(int selX1, int selY1, int selX2, int selY2, boolean joinBackLines) {
return getSelectedText(selX1, selY1, selX2, selY2, true, false);
}
public String getSelectedText(int selX1, int selY1, int selX2, int selY2, boolean joinBackLines, boolean joinFullLines) {
final StringBuilder builder = new StringBuilder();
final int columns = mColumns;
@@ -79,7 +95,9 @@ public final class TerminalBuffer {
}
if (lastPrintingCharIndex != -1)
builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1);
if (!rowLineWrap && row < selY2 && row < mScreenRows - 1) builder.append('\n');
boolean lineFillsWidth = lastPrintingCharIndex == x2Index - 1;
if ((!joinBackLines || !rowLineWrap) && (!joinFullLines || !lineFillsWidth)
&& row < selY2 && row < mScreenRows - 1) builder.append('\n');
}
return builder.toString();
}

View File

@@ -1267,6 +1267,7 @@ public final class TerminalEmulator {
break;
case 'c': // RIS - Reset to Initial State (http://vt100.net/docs/vt510-rm/RIS).
reset();
mMainBuffer.clearTranscript();
blockClear(0, 0, mColumns, mRows);
setCursorPosition(0, 0);
break;
@@ -1376,10 +1377,10 @@ public final class TerminalEmulator {
}
break;
case 'A': // "CSI${n}A" - Cursor up (CUU) ${n} rows.
setCursorRow(Math.max(mTopMargin, mCursorRow - getArg0(1)));
setCursorRow(Math.max(0, mCursorRow - getArg0(1)));
break;
case 'B': // "CSI${n}B" - Cursor down (CUD) ${n} rows.
setCursorRow(Math.min(mBottomMargin - 1, mCursorRow + getArg0(1)));
setCursorRow(Math.min(mRows - 1, mCursorRow + getArg0(1)));
break;
case 'C': // "CSI${n}C" - Cursor forward (CUF).
case 'a': // "CSI${n}a" - Horizontal position relative (HPR). From ISO-6428/ECMA-48.

View File

@@ -8,6 +8,7 @@ import android.system.Os;
import android.system.OsConstants;
import android.util.Log;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
@@ -339,4 +340,26 @@ public final class TerminalSession extends TerminalOutput {
return mShellPid;
}
/** Returns the shell's working directory or null if it was unavailable. */
public String getCwd() {
if (mShellPid < 1) {
return null;
}
try {
final String cwdSymlink = String.format("/proc/%s/cwd/", mShellPid);
String outputPath = new File(cwdSymlink).getCanonicalPath();
String outputPathWithTrailingSlash = outputPath;
if (!outputPath.endsWith("/")) {
outputPathWithTrailingSlash += '/';
}
if (!cwdSymlink.equals(outputPathWithTrailingSlash)) {
return outputPath;
}
} catch (IOException | SecurityException e) {
Log.e(EmulatorDebug.LOG_TAG, "Error getting current directory", e);
}
return null;
}
}

View File

@@ -133,8 +133,6 @@ public class CursorAndScreenTest extends TerminalTestCase {
withTerminalSized(3, 3).enterString("ABCDEFG\033[2AH").assertLinesAre("AHC", "DEF", "G ");
// If an attempt is made to move the cursor above the top margin, the cursor stops at the top margin:
withTerminalSized(3, 3).enterString("ABCDEFG\033[44AH").assertLinesAre("AHC", "DEF", "G ");
// Set top margin and validate that cursor does not go above it:
withTerminalSized(3, 3).enterString("\033[2rABCDEFG\033[44AH").assertLinesAre("ABC", "DHF", "G ");
}
public void testCursorDown() {
@@ -143,8 +141,6 @@ public class CursorAndScreenTest extends TerminalTestCase {
withTerminalSized(3, 3).enterString("AB\033[2BC").assertLinesAre("AB ", " ", " C");
// If an attempt is made to move the cursor below the bottom margin, the cursor stops at the bottom margin:
withTerminalSized(3, 3).enterString("AB\033[44BC").assertLinesAre("AB ", " ", " C");
// Set bottom margin and validate that cursor does not go above it:
withTerminalSized(3, 3).enterString("\033[1;2rAB\033[44BC").assertLinesAre("AB ", " C", " ");
}
public void testReportCursorPosition() {

View File

@@ -37,4 +37,12 @@ public class ScreenBufferTest extends TerminalTestCase {
withTerminalSized(5, 3).enterString("ABCDE\r\nFGHIJ").assertLinesAre("ABCDE", "FGHIJ", " ");
assertEquals("ABCDE\nFG", mTerminal.getSelectedText(0, 0, 1, 1));
}
public void testGetSelectedTextJoinFullLines() {
withTerminalSized(5, 3).enterString("ABCDE\r\nFG");
assertEquals("ABCDEFG", mTerminal.getScreen().getSelectedText(0, 0, 1, 1, true, true));
withTerminalSized(5, 3).enterString("ABC\r\nFG");
assertEquals("ABC\nFG", mTerminal.getScreen().getSelectedText(0, 0, 1, 1, true, true));
}
}

View File

@@ -107,4 +107,24 @@ public class ScrollRegionTest extends TerminalTestCase {
assertLinesAre("1 ", "2 ", "3 ", "QQ", "YY");
}
/** See https://github.com/termux/termux-app/issues/1340 */
public void testScrollRegionDoesNotLimitCursorMovement() {
withTerminalSized(6, 4)
.enterString("\033[4;7r\033[3;1Haaa\033[Axxx")
.assertLinesAre(
" ",
" xxx",
"aaa ",
" "
);
withTerminalSized(6, 4)
.enterString("\033[1;3r\033[3;1Haaa\033[Bxxx")
.assertLinesAre(
" ",
" ",
"aaa ",
" xxx"
);
}
}

View File

@@ -20,7 +20,7 @@ android {
compileSdkVersion 28
dependencies {
implementation "androidx.annotation:annotation:1.0.1"
implementation "androidx.annotation:annotation:1.1.0"
api project(":terminal-emulator")
}
@@ -44,7 +44,7 @@ android {
}
dependencies {
testImplementation 'junit:junit:4.12'
testImplementation 'junit:junit:4.13'
}
apply from: '../scripts/bintray-publish.gradle'

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,4 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@drawable/text_select_handle_left_mtrl_alpha"
android:tint="#2196F3" />
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="24dp"
android:viewportWidth="132"
android:viewportHeight="66">
<path
android:pathData="M52.3,1.6c-5.7,2.1 -12.9,8.6 -16,14.8 -2.2,4.1 -2.8,6.9 -3.1,14.3 -0.6,12.6 1.3,17.8 9.3,25.8 8,8 13.2,9.9 25.8,9.3 11.1,-0.5 17.3,-3.2 23.5,-10.3 6.5,-7.4 7.2,-10.8 7.2,-34.7l0,-20.8 -21.2,0.1c-16.1,-0 -22.3,0.4 -25.5,1.5z"
android:fillColor="#2196F3"
android:strokeColor="#00000000"/>
</vector>

View File

@@ -1,4 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@drawable/text_select_handle_right_mtrl_alpha"
android:tint="#2196F3" />
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="24dp"
android:viewportWidth="132"
android:viewportHeight="66">
<path
android:pathData="M33,20.8c0,23.9 0.7,27.3 7.2,34.7 6.2,7.1 12.4,9.8 23.5,10.3 12.6,0.6 17.8,-1.3 25.8,-9.3 8,-8 9.9,-13.2 9.3,-25.8 -0.5,-11.1 -3.2,-17.3 -10.3,-23.5 -7.4,-6.5 -10.8,-7.2 -34.7,-7.2l-20.8,-0 0,20.8z"
android:fillColor="#2196F3"
android:strokeColor="#00000000"/>
</vector>