Compare commits

..

209 Commits
v0.31 ... v0.50

Author SHA1 Message Date
Fredrik Fornwall
1a09b6d2a6 Change android:extractNativeLibs to false
We're trying this to see if that fixes an F-Droid build issue.

See #319.
2017-05-18 13:25:41 +02:00
Fredrik Fornwall
9d3f7734a1 Update android build tools 2017-05-16 11:11:57 +02:00
Fredrik Fornwall
61f766b59f Bump build tools in .travis.yml 2017-05-16 00:21:15 +02:00
Fredrik Fornwall
3f08376881 Bump version to 0.49 2017-05-15 23:42:55 +02:00
Fredrik Fornwall
cf06e70429 Update gradle configuration 2017-05-15 23:42:30 +02:00
Fredrik Fornwall
0714e435cb Do not show a Toast on clipboard text
Fixes https://github.com/termux/termux-app/issues/149.
2017-05-15 23:42:13 +02:00
Fredrik Fornwall
c26315185f Export TMPDIR to $PREFIX/tmp
Fixes https://github.com/termux/termux-packages/issues/1010.
Fixes https://github.com/termux/termux-app/issues/306.
2017-05-15 23:40:55 +02:00
Fredrik Fornwall
adc43c40c5 Perform haptic feedback on extra keys
Fixes #269.
2017-04-16 12:18:13 +02:00
Fredrik Fornwall
779b1ca1f8 Update the Android gradle plugin to 2.3.1 2017-04-05 09:10:54 +02:00
Fredrik Fornwall
e375f99b0c Remove float/ module from .idea/gradle.xml 2017-04-05 09:09:48 +02:00
Fredrik Fornwall
0ac55cd77a Remove float/
The Termux:Float app has a new home at:

https://github.com/termux/termux-float
2017-04-04 23:39:05 +02:00
Fredrik Fornwall
4aefa5049b Add gradle setup to publish to jcenter 2017-04-04 23:31:00 +02:00
Fredrik Fornwall
5b14124258 Make GestureAndScaleRecognizer non-public 2017-04-04 20:56:19 +02:00
Fredrik Fornwall
1a9c38374c Fix some invalid html in javadoc 2017-04-02 15:11:56 +02:00
Fredrik Fornwall
eefd504f08 Use getLocationOnScreen() to calculate position
This avoids problem when the window has been prevented from scrolling
outside of the screen, or when the touch keyboard has made it move.
2017-04-02 14:46:47 +02:00
Fredrik Fornwall
19f838e3d1 Show window if launched when hidden 2017-04-02 14:46:00 +02:00
Fredrik Fornwall
63adb2b132 Rename TerminalKeyListener to TerminalViewClient 2017-04-02 14:25:34 +02:00
Fredrik Fornwall
7d3a988d2f Restructure library modules
terminal/ -> terminal-emulator/
view/ -> terminal-view/
2017-04-02 14:21:36 +02:00
Fredrik Fornwall
5cd705d6d1 Reformat all code in float/, getting rid of tabs 2017-04-02 14:10:26 +02:00
Fredrik Fornwall
af8b269b51 Work on Termux:Float input handling 2017-04-02 14:09:43 +02:00
Fredrik Fornwall
8c220eaaea Do not call long press listener when scaling 2017-04-02 14:07:19 +02:00
Fredrik Fornwall
0142e1ed0e Update Termux:Float launcher icon 2017-04-02 07:33:06 +02:00
Fredrik Fornwall
ac0a349ed9 Remove versionCode&versionName from libraries 2017-04-02 07:12:03 +02:00
Fredrik Fornwall
548b0916b6 Clean up {terminal,view}/build.gradle 2017-04-02 06:58:59 +02:00
Fredrik Fornwall
009de5a3ee Split up into modules and add float module
Split the app/ module into three modules

terminal/ - Terminal emulator library module.
view/ - Terminal view library module (depending on terminal/).
app/ - The main Termux app (depending on view/).

Also add the

float/ - The Termux:Float app (depending on view/).
2017-04-01 19:06:02 +02:00
Fredrik Fornwall
41d0d60017 Respect content type termux-open for url:s 2017-03-28 23:52:52 +02:00
Fredrik Fornwall
12ac0fa73c Add android:extractNativeLibs="false" to manifest 2017-03-12 20:55:58 +01:00
Fredrik Fornwall
835dfc0276 Avoid synthetic accessor method 2017-03-06 01:53:09 +01:00
Fredrik Fornwall
f60316835f Fix some android studio lint warnings 2017-03-06 01:47:08 +01:00
Fredrik Fornwall
f74a091be6 Remove useless continue statement 2017-03-06 01:44:16 +01:00
Fredrik Fornwall
dd6cb5221d Work around Android < 7.0 wifi manager leak
http://tools.android.com/tech-docs/lint-in-studio-2-3#TOC-WifiManager-Leak
2017-03-06 01:42:33 +01:00
Fredrik Fornwall
cab6df5c0e Update gradle config 2017-03-06 01:41:12 +01:00
Fredrik Fornwall
f6f0809558 Bump version to 0.48 2017-03-02 00:32:09 +01:00
Fredrik Fornwall
11ed7e45d8 Fix crash when opening URL:s in Android < 7.0
In versions of Android earlier than 7.0 the FLAG_ACTIVITY_NEW_TASK
is needed when starting an activity from a non-activity context.

This was removed in Android 7.0 (possibly by mistake), see
https://code.google.com/p/android/issues/detail?id=226647.

Fixes #802
2017-03-02 00:29:17 +01:00
Fredrik Fornwall
ed1874db05 Bump version to 0.47 2017-02-28 00:47:16 +01:00
Fredrik Fornwall
cb60803a80 Tweak launcher size 2017-02-28 00:46:21 +01:00
Fredrik Fornwall
fc92a27cb2 Extended keyboard: allow inline input
Ff the buffer is empty, send a newline, otherwise send the content of
the buffer with the newline stripped. This way <Enter><Enter> means
"insert the buffer content and send a newline", while a single <Enter>
means just "insert the buffer content". Fixes #261.
2017-02-28 00:42:36 +01:00
Fredrik Fornwall
29e62e608f Use standard actions 2017-02-28 00:42:07 +01:00
Fredrik Fornwall
8a7f93d722 Launch $PREFIX/bin/login by default
The login command takes care of both showing the etc/motd and
launching the login shell.
2017-02-28 00:41:20 +01:00
Fredrik Fornwall
420683fe65 Add built-in broadcast receiver to open files 2017-02-27 22:31:11 +01:00
Fredrik Fornwall
a3256ed551 Travis testing 2017-02-05 22:25:37 +01:00
Fredrik Fornwall
57add98e3c More attempts to get coverity scan working 2017-02-05 21:44:47 +01:00
Fredrik Fornwall
6e5c04e04f Remove the travis certificate workaround 2017-02-05 18:24:59 +01:00
Fredrik Fornwall
528a05ef61 Remove the welcome dialog
Instead of a modal dialog to be dismissed without any way to recall
it, the initial information will be shown in the terminal itself.
2017-02-04 11:31:04 +01:00
Fredrik Fornwall
cb2f892dc5 Adjust text placement on feature graphics 2016-12-30 03:23:53 +01:00
Fredrik Fornwall
8b6e8d7fdd Bump version to 0.46 2016-12-30 02:43:40 +01:00
Fredrik Fornwall
d0eeaa9fc3 Call completeWakefulIntent() in Termux service 2016-12-30 02:42:41 +01:00
Fredrik Fornwall
b793913481 Style for android tv launcher transition 2016-12-30 02:39:26 +01:00
Fredrik Fornwall
d48b438c40 Test wcwidth on two more code points 2016-12-30 02:39:01 +01:00
Fredrik Fornwall
11afe895e1 Try with a bit shorter process completed messages 2016-12-30 02:38:41 +01:00
Fredrik Fornwall
bc96f71a2d Update the tv banner a bit 2016-12-30 02:34:43 +01:00
Fredrik Fornwall
87dfded5e6 Remove unused on_bell.xml file 2016-12-30 01:54:31 +01:00
Fredrik Fornwall
7c0ae4cb54 Remove the bell shake animation
Fixes https://github.com/termux/termux-packages/issues/628
Fixes https://github.com/termux/termux-app/issues/222
2016-12-30 01:53:07 +01:00
Fredrik Fornwall
b917acbbfa Do not use IME_ACTION_NONE in the inputconnection
Using IME_ACTION_NONE prevents enter key to be used with the stock
Android TV keyboard.

Fixes #221.
2016-12-30 01:42:47 +01:00
Fredrik Fornwall
7d9d6fb797 Translate \n to \r when receiving text from an IME
Fixes issue with fzf and return using the stock cyanogenmod
keyboard reported by @mklein994 on gitter.
2016-12-30 00:45:11 +01:00
Fredrik Fornwall
74040dd37f Add feature and tv banner to art/ 2016-12-29 22:29:37 +01:00
Fredrik Fornwall
e94f06d0f7 Silence lint warnings in shortcuts.xml 2016-12-28 02:45:14 +01:00
Fredrik Fornwall
af21b6dc3e Remove unused variable. Use Collections.addAll() 2016-12-28 01:19:57 +01:00
Fredrik Fornwall
e0e8257f1c Update build dependencies 2016-12-28 01:07:12 +01:00
Fredrik Fornwall
743b067cae Bump version to 0.45 2016-12-27 10:43:05 +01:00
Fredrik Fornwall
23333c074a Fix NPE regression in version 0.44 2016-12-27 10:42:41 +01:00
Fredrik Fornwall
f11644fa51 travis: Update build tools 2016-12-26 05:18:45 +01:00
Fredrik Fornwall
212be59fca gradle: Update build tools 2016-12-26 04:48:28 +01:00
Fredrik Fornwall
e3a1f8224f Add art/copy-to-other-apps.sh script 2016-12-26 04:45:00 +01:00
Fredrik Fornwall
4f40d5a26a Bump version to 0.44 2016-12-26 04:07:44 +01:00
Fredrik Fornwall
df92896eef Add Android 7.1 launcher shortcut for new session 2016-12-26 04:07:03 +01:00
Fredrik Fornwall
4c93cb42f1 Use constant for intent extra key 2016-12-26 00:57:09 +01:00
Fredrik Fornwall
34afb9de43 Give up on using vector drawables for launch icons
Instead use svg files in art/ and a script to build the png files.
2016-12-24 01:05:33 +01:00
Fredrik Fornwall
b6ea29d260 Remove duplicate updateNotification() calls 2016-12-24 00:25:24 +01:00
Fredrik Fornwall
289d58a2f0 Try it with launcher icons in drawable-anydpi 2016-12-10 22:51:11 -05:00
Fredrik Fornwall
0501ce924b gradle: Update android gradle plugin 2016-12-07 20:26:09 -05:00
Fredrik Fornwall
d12256f5e5 Add flags to imeOptions
Add IME_FLAG_NO_ENTER_ACTION and IME_ACTION_NONE. I haven't
encountered any issue without them, but specifying them is correct.
2016-12-04 17:32:40 +01:00
Fredrik Fornwall
fcbc036f92 Update gradle to 3.2.1 2016-12-04 17:26:26 +01:00
Fredrik Fornwall
70d5839334 Bump version to 0.43 for preview testing 2016-12-04 04:38:35 +01:00
Fredrik Fornwall
9c19540759 Keep track of background tasks
Also merge wake lock and wifi lock into one, and make it exposed to
termux-wake-lock and termux-wake-unlock commands.
2016-12-04 04:37:13 +01:00
Fredrik Fornwall
9fe0e49473 Render cursor color&shapes (underline and ibeam)
Allow the cursor to be colored by the theme, and support rendering
underline and ibeam cursor styles.
2016-12-04 01:06:50 +01:00
Fredrik Fornwall
357b17e972 Improve setup of symlinks to external storage
The context.getExternalFilesDirs(null) call may return several
elements, and some of them may be null.
2016-12-04 01:04:19 +01:00
Fredrik Fornwall
6334470f81 Try to handle Samsung keyboard better
The stock Samsung keyboard with 'Auto check spelling' enabled may
send multiple backspaces. Note that this auto-correction of
spelling will not work good in general with a terminal, so should
be disabled (or another keyboard used) when using Termux.
2016-11-28 00:26:37 +01:00
Fredrik Fornwall
b8cdd59c68 Use gradle build, not assembly, for coverity 2016-11-25 03:55:07 +01:00
Fredrik Fornwall
6cf36fffd7 Fix typo on coverity setup 2016-11-25 03:43:16 +01:00
Fredrik Fornwall
f10ecd4db5 Try to fix coverity scans in travis 2016-11-25 03:17:50 +01:00
Fredrik Fornwall
26457e8443 Tweak fillcolor in icons for easier svg generation 2016-11-25 02:14:54 +01:00
Fredrik Fornwall
6702846c7c Update build tools version in travis 2016-11-24 23:58:43 +01:00
Fredrik Fornwall
0c6180bbb1 Update gradle deps 2016-11-24 23:26:27 +01:00
Fredrik Fornwall
fcf07f6a19 Coverity: Specify "branch_pattern: master" 2016-11-24 20:15:31 +01:00
Fredrik Fornwall
e272e3b3b2 Update README a bit 2016-11-24 20:05:28 +01:00
Fredrik Fornwall
cdf0e72145 Enable coverity scan for all branches 2016-11-24 19:36:38 +01:00
Fredrik Fornwall
70245eb78c Use BackgroundJob.setupProcessArgs for sessions 2016-11-23 01:55:29 +01:00
Fredrik Fornwall
ee7631dfac Add a round icon for android-25 2016-11-23 01:38:17 +01:00
Fredrik Fornwall
5ecf5d12d1 Tweak the icon somewhat 2016-11-23 01:37:58 +01:00
Fredrik Fornwall
0c8cd90f4e Use $PREFIX/bin/sh for script file without shebang
Also try to handle #!(/usr)/bin/foo shebangs.
2016-11-20 16:43:27 +01:00
Fredrik Fornwall
e1ea68913f Use to a vector notification icon 2016-11-19 22:33:45 +01:00
Fredrik Fornwall
dde854eba7 Switch to a vector icon 2016-11-19 22:18:26 +01:00
Fredrik Fornwall
07a4607c04 Merge pull request #193 from friederbluemle/update-gradle-wrapper
Update Gradle wrapper to 3.2
2016-11-19 21:04:10 +01:00
Fredrik Fornwall
883be37b98 Add a shake animation on a terminal bell 2016-11-19 15:41:24 +01:00
Frieder Bluemle
d939d3d927 Update Gradle wrapper to 3.2 2016-11-18 18:01:00 +08:00
Fredrik Fornwall
a0fa51eb92 Start work on background jobs 2016-10-26 02:26:44 +02:00
Fredrik Fornwall
755513bb33 Update gradle setup 2016-10-20 01:00:28 +02:00
Fredrik Fornwall
8ad7a6669c Switch a commit() to apply() for shared prefs 2016-10-16 00:13:35 +02:00
Fredrik Fornwall
60f7aada9e Remove logging at info level 2016-10-16 00:13:04 +02:00
Fredrik Fornwall
6aa0492434 Add @NonNull annotation 2016-10-16 00:12:42 +02:00
Fredrik Fornwall
019aa44837 Update Android Gradle Plugin 2016-10-10 22:23:39 +02:00
Fredrik Fornwall
8d3d5e147f Remove outdated JNI building instructions 2016-09-29 01:13:45 +02:00
Fredrik Fornwall
44197b90e2 Update .travis.yml 2016-09-29 00:34:12 +02:00
Fredrik Fornwall
794c7ee333 Remove deprecated android:singleLine attribute 2016-09-27 00:30:05 +02:00
Fredrik Fornwall
0457ddbc69 Update build.gradle with latest versions 2016-09-27 00:29:47 +02:00
Fredrik Fornwall
283792af5e Refresh listview after changing session name 2016-09-27 00:29:08 +02:00
Fredrik Fornwall
be7cfa603a Fix gradle syntax for Android Studio 2016-09-25 01:15:49 +02:00
Fredrik Fornwall
3480bf7346 Switch from cmake to ndk-build
We switch from cmake to ndk-build to make it easier for builders
in not requiring an additional tool installed. The JNI build is
so simple so we don't really need much of a build tool anyway.
2016-09-24 20:22:10 +02:00
Fredrik Fornwall
8314a2756c Setup NDK in travis 2016-09-23 08:57:40 +02:00
Fredrik Fornwall
4de82d9fe0 Update gradle to 3.1 2016-09-21 22:34:44 +02:00
Fredrik Fornwall
d658e16801 Use the new cmake support to build JNI code 2016-09-21 22:30:20 +02:00
Fredrik Fornwall
26dcd5af88 Update gradle and Android Studio 2016-09-19 23:26:49 +02:00
Fredrik Fornwall
8056013082 Bump version to 0.42 2016-09-16 23:18:51 +02:00
Fredrik Fornwall
8e90545c4b Remove comment from build.gradle 2016-09-16 23:18:34 +02:00
Fredrik Fornwall
426ddbacbd Remove useless casts 2016-09-16 23:17:47 +02:00
Fredrik Fornwall
7e1f8a551f Change VolumeUp+H to generate ~
Using VolumeUp+H to generate a tilde (~) is more useful than
sending the home key.

Fixes #151.
2016-09-16 23:15:27 +02:00
Fredrik Fornwall
e169af0447 Change shortcuts from Ctrl+Shift to Ctrl+Alt
This works on more language layouts and devices.

Fixes #145.
2016-09-16 23:12:56 +02:00
Fredrik Fornwall
a2cb3fafee Fix numpad 0 and . key handling
Fixes #146.
2016-09-05 23:11:25 +02:00
Fredrik Fornwall
166710f14a Bump version to 0.40 2016-09-04 19:00:29 +02:00
Fredrik Fornwall
c1a9b7726f Tweak InputConnection implementation 2016-09-04 18:56:28 +02:00
Fredrik Fornwall
afb339e9d8 Format code 2016-08-30 13:47:30 +02:00
Fredrik Fornwall
64c23f498f Implement true (24-bit) color 2016-08-27 00:32:38 +02:00
Fredrik Fornwall
1dc92b2a12 Remove unused imports 2016-08-22 16:54:55 +02:00
Fredrik Fornwall
990a957383 Bump android support library version 2016-08-22 16:53:42 +02:00
Fredrik Fornwall
7bb64d724c Update android plugin for gradle 2016-08-16 10:31:10 +02:00
Fredrik Fornwall
4609dd71c6 Switch KEYCODE_HOME -> KEYCODE_MOVE_HOME in tests 2016-08-12 06:13:26 +02:00
Fredrik Fornwall
8d00f22d4c Catch KEYCODE_MOVE_HOME and not KEYCODE_HOME
The KEYCODE_HOME event is handled by the system and never delivered
to applications, it's KEYCODE_MOVE_HOME (FN+LeftArrow) we want to
handle ourselves and send as an escape sequence.

Fixes #138.
2016-08-12 04:18:09 +02:00
Fredrik Fornwall
5532421ab2 Check arches in order of preference
The documentation for Build.SUPPORTED_ABIS says:
"An ordered list of ABIs supported by this device. The most preferred
ABI is the first element in the list."

Respect that preference when checking for which arch to install
packages for.

Fixes #131.
2016-08-08 23:22:47 +02:00
Fredrik Fornwall
d2b27978e2 Bump version for v0.39 2016-08-08 23:19:49 +02:00
Fredrik Fornwall
30b05e9ab2 Merge pull request #132 from michalbednarski/extrakeysview-alignment
Make ExtraKeysView work on Android 5
2016-08-08 23:10:10 +02:00
Michał Bednarski
c350318c77 Make ExtraKeysView work on Android 5
This is done by explicitly specifying alignment as GridLayout.FILL
as I have figured out that this was fixed in Android 6 in commit
6dafd87fb4%5E%21/#F0
which set default alignment to FILL if weight is nonzero
2016-08-08 10:16:47 +02:00
Fredrik Fornwall
c9b49cef58 Bump version to 0.38 2016-08-05 00:00:53 +02:00
Fredrik Fornwall
f9c642c672 Support Unicode 9 for wcwidth (don't squash emojis) 2016-08-04 23:58:09 +02:00
Fredrik Fornwall
c0a5e5f57a Switch to TYPE_NULL as input type
This fixes #126 where the previous input type put some keyboards into
word mode (no direct echo). The workaround for Google Pinyin does not
seem to be necessary no more.

Also fix backspace after entering emojis on some keyboards (Swype).
2016-08-04 23:56:17 +02:00
Fredrik Fornwall
dfdc9b37e1 Allow predictive text area input to remove session
Fixes #124
2016-08-04 23:27:42 +02:00
Fredrik Fornwall
dfb22e6050 Make user-configurable shortcuts case insensitive 2016-08-04 18:11:50 +02:00
Fredrik Fornwall
b95d84fe13 Use absolute reference for android.R (lint check) 2016-08-02 17:34:13 +02:00
Fredrik Fornwall
a73228b109 Fix Enter to finish session in more cases
Detect the Enter key to finish a session not only on KeyEvent:s,
but also when the IME uses InputConnection.commitText() to send
\n. This seems to be triggered more after switching to the Uri
input type. Closes #124

Also bump app version for a quick release.
2016-08-01 06:37:49 +02:00
Fredrik Fornwall
eaeb0930f4 Enable x86_64 and bump version code 2016-08-01 01:47:51 +02:00
Fredrik Fornwall
95a50096cb Remove attempt of icons at popup menu actions 2016-08-01 00:30:11 +02:00
Fredrik Fornwall
8caeab470e Tweak the IME mode (from password to URI)
This makes more sense and avoids the extra number row when using
the Google keyboard. Closes #87.
2016-07-31 22:55:54 +02:00
Fredrik Fornwall
6b62e65154 Some minor AS lint warnings tweaks 2016-07-31 22:28:17 +02:00
Fredrik Fornwall
fb7dc21c18 Prepare for final build 2016-07-31 22:13:50 +02:00
Fredrik Fornwall
d0abd17091 Add version check before field usage 2016-07-31 22:13:24 +02:00
Fredrik Fornwall
0550dbff9d Fix backspace in combination with Alt and Ctrl 2016-07-27 00:27:21 +02:00
Fredrik Fornwall
9d7ed21f27 Fix Ctrl+/ to send same as Ctrl+_ 2016-07-27 00:26:50 +02:00
Fredrik Fornwall
7e2cbd969a Update android build tools to 24.0.1 2016-07-23 17:09:15 +02:00
Fredrik Fornwall
f9842f22fb Update gradle wrapper to 2.14.1 2016-07-23 17:08:54 +02:00
Fredrik Fornwall
962a43743c Update for android studio file 2016-07-23 16:52:49 +02:00
Fredrik Fornwall
ef892fca0b Update versions of support dependencies 2016-07-23 16:52:24 +02:00
Fredrik Fornwall
2bf9e7b205 Do not send mouse up event after scrolling
This fixes an issue where e.g. in tmux, when pressing the finger
on an upper pane and dragging it down to scroll and releasing it
further down on another pane, the upper pane lost focus to to a
mouse click being sent to the pane below.
2016-07-22 00:47:47 +02:00
Fredrik Fornwall
bc158252d6 Use static imports throughout 2016-07-22 00:23:23 +02:00
Fredrik Fornwall
b16f11cd87 Formatting for .travis.yml 2016-07-04 23:08:22 +02:00
Fredrik Fornwall
f57232b40e Use jdk8 for travis build 2016-07-04 22:55:24 +02:00
Fredrik Fornwall
f156ce259e Update travis configuration for androi-24 2016-07-04 22:24:38 +02:00
Fredrik Fornwall
2db6923bc4 Reformat code project-wide (getting rid of tabs) 2016-06-28 01:03:03 +02:00
Fredrik Fornwall
d72fd579ee Various updates mainly for extra keys 2016-06-28 00:56:30 +02:00
Fredrik Fornwall
964c0b7b4f Cleanup imports 2016-06-26 22:39:46 +02:00
Fredrik Fornwall
a049ea50d7 Update android studio lint configurations 2016-06-26 22:38:52 +02:00
Fredrik Fornwall
95a0878e10 Update gradle configuration 2016-06-26 22:38:36 +02:00
Fredrik Fornwall
5566b13073 Remove stray character 2016-06-26 22:37:12 +02:00
Fredrik Fornwall
9519727f38 Enable installation of x86-64 packages 2016-06-22 01:31:21 +02:00
Fredrik Fornwall
33d1477d4a Remove KeyboardModifiers 2016-06-22 00:24:42 +02:00
Fredrik Fornwall
1cc7829847 Update version 2016-06-22 00:24:18 +02:00
Fredrik Fornwall
d17bbab8ee Strings update for process killing 2016-06-22 00:23:57 +02:00
Fredrik Fornwall
a020d7c484 Add a wcwidth test 2016-06-22 00:23:38 +02:00
Fredrik Fornwall
9be6470d19 Add .idea/inspectionProfiles/ 2016-06-22 00:23:18 +02:00
Fredrik Fornwall
491240ee3f Fix MockTerminalOutput to implement all methods 2016-06-08 16:09:42 +02:00
Fredrik Fornwall
599aaff723 Update android gradle plugin 2016-06-08 16:09:23 +02:00
Fredrik Fornwall
20d57908a7 Make cursor visible by forcing to text color 2016-06-08 02:13:14 +02:00
Fredrik Fornwall
2104252244 Change session exit detection
Previously we waited for all opened file descriptors to the terminal
to be closed. This caused problem when e.g. running "sleep 900 &"
and then exiting the shell, with sleep keeping the session alive and
had to be killed manually (killing the process group did not help -
the shell had already exited and was in zombie state). This is also
what most other terminal emulators do.

Relatedly, switch to sending SIGKILL to force quit a session instead
of SIGHUP, since SIGHUP can be ignored.
2016-06-08 01:37:08 +02:00
Fredrik Fornwall
f047160fd6 Tweak layout for extra keys view 2016-06-06 01:16:20 +02:00
Fredrik Fornwall
a2ebcdcf49 Extra keys view: Implement sending text 2016-06-06 00:56:42 +02:00
Fredrik Fornwall
0861be363b Remove some inspect code warnings 2016-05-20 10:46:48 +02:00
Fredrik Fornwall
d1c0b6abdc Add initial support for extra keys 2016-05-20 10:44:23 +02:00
Fredrik Fornwall
8714800c6b Add an extra keys view 2016-05-20 10:41:38 +02:00
Fredrik Fornwall
042fbfaea3 TerminalView: Start support for extra keys 2016-05-20 10:41:07 +02:00
Fredrik Fornwall
08d6d1706d Add pref for showing extra keys 2016-05-20 10:36:20 +02:00
Fredrik Fornwall
cf19d43bb7 Gradle build updates
- Switch to using gradle to build jni lib.
- Enable proguard minification.
- Add the Android support library.
2016-05-20 10:30:25 +02:00
Fredrik Fornwall
f86c7a85d3 Update .idea config 2016-05-20 10:10:14 +02:00
Fredrik Fornwall
887d7810f6 Update build tools versions for travis 2016-05-20 10:09:39 +02:00
Fredrik Fornwall
5be3099a5b Update build tools SDK version 2016-05-13 00:18:51 +02:00
Fredrik Fornwall
bdd5c80fca Commit the text on finishComposingText()
This handles e.g. text written with hand writing input methods
as mentioned in #91.
2016-05-09 15:39:11 +02:00
Fredrik Fornwall
cc7b6cba13 Change minimum cols&rows from 8 to 4
This avoids e.g. the keyboard overlapping the terminal in setups
that can actually happen. Closes #88.
2016-05-04 22:44:38 +02:00
Fredrik Fornwall
ff2f77c427 Mark reset() private 2016-05-04 22:27:43 +02:00
Fredrik Fornwall
afaa91b2ca Update gradle 2016-04-28 11:07:43 +02:00
Fredrik Fornwall
46da1fc833 termux.c: Re-indent whole file with vim 2016-04-23 12:21:36 +02:00
Fredrik Fornwall
746dc750df Bump version to 0.34 2016-04-22 03:48:28 +02:00
Fredrik Fornwall
7db1f6c5a1 Add explicit handling of switch constant 2016-04-22 03:30:09 +02:00
Fredrik Fornwall
b7f3fdf528 Update android-annotations version 2016-04-22 03:28:50 +02:00
Fredrik Fornwall
6e7f777d04 Remove support for ACTION_GET_CONTENT
As we have a document provider now we can remove ACTION_GET_CONTENT.

"The ACTION_OPEN_DOCUMENT intent is only available on devices running
Android 4.4 and higher. If you want your application to support
ACTION_GET_CONTENT to accommodate devices that are running Android 4.3
and lower, you should disable the ACTION_GET_CONTENT intent filter in
your manifest for devices running Android 4.4 or higher. A document
provider and ACTION_GET_CONTENT should be considered mutually exclusive.
If you support both of them simultaneously, your app will appear twice
in the system picker UI, offering two different ways of accessing your
stored data. This would be confusing for users."

- http://developer.android.com/guide/topics/providers/document-provider.html#43
2016-04-22 03:26:23 +02:00
Fredrik Fornwall
fc15bd2355 Documents provider: Remove FLAG_SUPPORTS_RECENTS 2016-04-22 02:51:13 +02:00
Fredrik Fornwall
a87cbdd70c Make VolumeUp+x send Alt+x 2016-04-22 02:20:10 +02:00
Fredrik Fornwall
fb7f7d249e Ignore warnings about setExecutable(true) failing 2016-04-22 02:17:41 +02:00
Fredrik Fornwall
026d0b495e Expose files through the Storage Access Framework
This allows e.g. external editors to edit files in the Termux home
folder. Fixes #79.
2016-04-22 02:13:50 +02:00
Fredrik Fornwall
533fa60516 Remove unused imports 2016-04-22 02:00:36 +02:00
Fredrik Fornwall
dc086a1e0b Tweak button ordering on the file received dialog 2016-04-16 23:02:20 +02:00
Fredrik Fornwall
2a056aeb2e Match less intents for receiving files
Be liberal when accepting SEND intents, but restrict to text files
for VIEW intents.
2016-04-16 23:01:19 +02:00
Fredrik Fornwall
9e70ebc2a6 Build native libraries for 64-bit arm 2016-04-16 21:22:04 +02:00
Fredrik Fornwall
9686127f81 Fix installer to check supported abi:s
This fixes installation on e.g. the Samsung Galaxy S5 Neo which has
a 64-bit cpu but no 64-bit runtime available (closes #69).
2016-04-16 21:18:21 +02:00
Fredrik Fornwall
395c36ee83 Fix typo 2016-04-11 14:13:02 +02:00
Fredrik Fornwall
906ff24e76 Avoid repetition of home path constant 2016-04-11 14:12:21 +02:00
Fredrik Fornwall
c8af974852 Bump version in preparation for 0.33 2016-04-11 14:06:23 +02:00
Fredrik Fornwall
481339e2f5 Do not force chooser when opening url 2016-04-11 14:05:31 +02:00
Fredrik Fornwall
b2ecae63a8 Update to Android Studio 2.0 2016-04-11 13:50:14 +02:00
Fredrik Fornwall
a67f798f2f Update the android gradle plugin 2016-04-11 13:48:24 +02:00
Fredrik Fornwall
d69485b70b Match less files for file receiving (fixes #66) 2016-03-25 00:22:49 +01:00
Fredrik Fornwall
421dfcca39 Do not fail with NPE when scheme is null
Also remove some debug logging left by mistake.
2016-03-23 18:31:03 +01:00
142 changed files with 9543 additions and 10876 deletions

1
.gitignore vendored
View File

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

View File

@@ -77,7 +77,7 @@
<match> <match>
<AND> <AND>
<NAME>xmlns:android</NAME> <NAME>xmlns:android</NAME>
<XML_NAMESPACE>Namespace:</XML_NAMESPACE> <XML_NAMESPACE>^$</XML_NAMESPACE>
</AND> </AND>
</match> </match>
</rule> </rule>
@@ -87,7 +87,7 @@
<match> <match>
<AND> <AND>
<NAME>xmlns:.*</NAME> <NAME>xmlns:.*</NAME>
<XML_NAMESPACE>Namespace:</XML_NAMESPACE> <XML_NAMESPACE>^$</XML_NAMESPACE>
</AND> </AND>
</match> </match>
<order>BY_NAME</order> <order>BY_NAME</order>
@@ -224,6 +224,6 @@
</codeStyleSettings> </codeStyleSettings>
</value> </value>
</option> </option>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="TermuxCodeStyle" /> <option name="PREFERRED_PROJECT_CODE_STYLE" value="Default (1)" />
</component> </component>
</project> </project>

4
.idea/gradle.xml generated
View File

@@ -6,13 +6,15 @@
<option name="disableWrapperSourceDistributionNotification" value="true" /> <option name="disableWrapperSourceDistributionNotification" value="true" />
<option name="distributionType" value="DEFAULT_WRAPPED" /> <option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="1.8" />
<option name="modules"> <option name="modules">
<set> <set>
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" /> <option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/terminal-emulator" />
<option value="$PROJECT_DIR$/terminal-view" />
</set> </set>
</option> </option>
<option name="resolveModulePerSourceSet" value="false" />
</GradleProjectSettings> </GradleProjectSettings>
</option> </option>
</component> </component>

View File

@@ -1,15 +1,71 @@
<component name="InspectionProjectProfileManager"> <component name="InspectionProjectProfileManager">
<profile version="1.0"> <profile version="1.0">
<option name="myName" value="Project Default" /> <option name="myName" value="Project Default" />
<inspection_tool class="AndroidLintGoogleAppIndexingWarning" enabled="false" level="WARNING" enabled_by_default="false" /> <inspection_tool class="AndroidLintLogConditional" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="EmptyStatementBody" enabled="true" level="WARNING" enabled_by_default="true"> <inspection_tool class="AndroidLintNegativeMargin" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AssignmentUsedAsCondition" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="DeprecatedAPI" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="DuplicateSwitchCase" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="EmptyStatementBody" enabled="false" level="WARNING" enabled_by_default="false">
<option name="m_reportEmptyBlocks" value="true" /> <option name="m_reportEmptyBlocks" value="true" />
<option name="commentsAreContent" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="EndlessLoop" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="EqualityInConditionalOperator" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="Finalize" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreTrivialFinalizers" value="true" />
</inspection_tool>
<inspection_tool class="FinalizeNotProtected" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="FormatSpecifiers" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="FunctionImplicitDeclarationInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HidesUpperScope" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HidingNonVirtualFunction" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ImplicitIntegerAndEnumConversion" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ImplicitPointerAndIntegerConversion" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="IncompatibleEnums" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="IncompatibleInitializers" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="IncompatiblePointers" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="InstanceofChain" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreInstanceofOnLibraryClasses" value="false" />
</inspection_tool>
<inspection_tool class="KRUnspecifiedParameters" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="LocalValueEscapesScope" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="LoggerInitializedWithForeignClass" enabled="false" level="WARNING" enabled_by_default="false">
<option name="loggerClassName" value="org.apache.log4j.Logger,org.slf4j.LoggerFactory,org.apache.commons.logging.LogFactory,java.util.logging.Logger" />
<option name="loggerFactoryMethodName" value="getLogger,getLogger,getLog,getLogger" />
</inspection_tool>
<inspection_tool class="MissingReturn" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="MissingSwitchCase" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="NotImplementedFunctions" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="NotInitializedVariable" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="NotSuperclass" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="OCDFAInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="OCLoopDoesntUseConditionVariableInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="OCSimplifyInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="OCUnusedGlobalDeclarationInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="OCUnusedMacroInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="OCUnusedStructInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="OCUnusedTemplateParameterInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="OnDemandImport" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PrivateMemberAccessBetweenOuterAndInnerClass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ResourceNotFoundInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SamePackageImport" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SignednessMismatch" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false"> <inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" /> <option name="processCode" value="true" />
<option name="processLiterals" value="true" /> <option name="processLiterals" value="true" />
<option name="processComments" value="true" /> <option name="processComments" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="UnnecessaryFullyQualifiedName" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_ignoreJavadoc" value="false" />
</inspection_tool>
<inspection_tool class="UnreachableCode" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UnusedExpressionResult" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UnusedImportStatement" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UnusedLocalVariable" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UnusedLocalization" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UnusedParameter" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UnusedValue" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ValueMayNotFitIntoReceiver" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="VariableNotUsedInsideIf" enabled="true" level="WARNING" enabled_by_default="true" />
</profile> </profile>
</component> </component>

View File

@@ -1,20 +1,25 @@
language: android
sudo: false sudo: false
language: android
jdk: oraclejdk8
env: env:
global: global:
# The next declaration is the encrypted COVERITY_SCAN_TOKEN, created # The next declaration is the encrypted COVERITY_SCAN_TOKEN, created
# via the "travis encrypt" command using the project repo's public key # via the "travis encrypt" command using the project repo's public key
- secure: "ACnFJxw0VusS2lnGXL+epP/CNJmftWS39YcPdgN2EurWw5ZfXSo7vi+zpMB+11IBS3LQyLFFUambi2N9L4lbReZkHVkoVcZFGZlwbXNTAeqT8CABPTcuOyEOZU4bJwqeYU87ztYipENMLNECaZrgWx5odbWLKnSJQw7Zkb4ArCstfXfYk9u8q49ThRxQyGwHW2xKp1an5aa+3Y6IY+ywsSHw6AvXbyFH078Kolxy86caagczcfmKcMi15QYzwAvFggUphvsO3M5PHJMQXuaNlQxDcQRGUEXsK8aZE0dPH5PB97SFjDALZqI7NEpjZAk5htWjX48ssW064LDbjcBg/ZLgDd8R8uhA159NVZgvcnP2czCn6pmggx1sW5MBmcj7i+bJS2ejaMO+KoovWlVvsch742H5QR6rQaNkjDZRsGVLYvJaR1gBLs898UoT1hcHWoqLVR22r2VFo7OWWCRfNRvZuZDR2HIrYRdFvn8P3nWVMkvXwgsOlxWG5sN+yQqW+6lZS7hivsFhtYs4CkRdoZIan3Qvi/CkY8Lg+ESkZ3IJ0NnId8qOWH+8Xl1sqZ7xlsWTd1sYYHlpvkdvqw1HNLP22EpwwKW5Kb5zBEd/qs3o1OO0Tqa0MR6JpgGdHHRk1iZ25+qTfRVP06vO2RXsgAx4SZfO7DyB0QZn8tGNMMI=" - secure: "LdajbHNfRlpnqzhX5KY2Vr7KtzU9vXDs1TCNn93J6Dt522f2AaiyUDJvISvz+uslk0WJiS5bB5vGwQmXginxz6Qi6uMgMbjWXulv1vfs6ZviKpUX348DOp1qKPa8WfVNB66F84SwGIfc8cRMAgCFw79l/DFgLErubF8vKo1wZ8Hmvrz//+RJ0BGMa3YRc4VyJhAL0P+0Wc1Q2Im7R9EovAxC5pZXBIMSgr6g5GzLWPisbNLXpMPGsDeYhcenO6XCtCCy+aNxUYM8vcrLDzlVXR5Hy7KEs/MGRTS0Yk13TWUEYa5wBpKelFTszdWYLVn5ANreh/aXRVfHpnW3epotMYguLx1kSvOhWEnc4F+qqv3nle2LpDg9Y9bcLyTTcYnPl9smqEVVjEDu0FoIr1V58xkG4Oc6BPIvLRjlMVU96PXh2HxMLuGsJ/xM+uAFU9oVMbC07xn42Eu5O4NHOHJNOwMWac4/lSKRK8W/7/vWuXj5vhkD9ZsGVpN70UtY5HAfNUGADnTeDblvjgFTNZ2mUN/u0o7Z8ZFURYllZ9YU+Vr2nPf9CAhVBjuwFWx8uRQpAg1aDmc1dVMJijRBeBeU/uWhYqsGp34wkNEl8VGzob4R4QTyI8+T7CndGqKVmbTK/SjqKhjjPpbXIAfOH+JtxvAnNmb8XeQSJ32uK2nexFo="
android: android:
components: components:
- platform-tools - platform-tools
- tools - tools
- build-tools-23.0.2 - build-tools-25.0.3
- android-23 - android-25
- extra-android-m2repository - extra-android-m2repository
before_install:
- git clone https://github.com/urho3d/android-ndk.git $HOME/android-ndk
- export ANDROID_NDK_HOME=$HOME/android-ndk
script: script:
- ./gradlew testDebugUnitTest - ./gradlew testDebugUnitTest
@@ -25,5 +30,5 @@ addons:
description: "Terminal emulator and Linux environment for Android" description: "Terminal emulator and Linux environment for Android"
notification_email: fredrik@fornwall.net notification_email: fredrik@fornwall.net
build_command_prepend: "./gradlew clean" build_command_prepend: "./gradlew clean"
build_command: "./gradlew assemble" build_command: "./gradlew build"
branch_pattern: coverity_scan branch_pattern: master

3
LICENSE.md Normal file
View File

@@ -0,0 +1,3 @@
Released under [the GPLv3 license](https://www.gnu.org/licenses/gpl.html).
Contains code from `Terminal Emulator for Android` by which is released under [the Apache License 2.0](https://www.apache.org/licenses/).

View File

@@ -3,24 +3,14 @@ Termux app
[![Travis build status](https://travis-ci.org/termux/termux-app.svg?branch=master)](https://travis-ci.org/termux/termux-app) [![Travis build status](https://travis-ci.org/termux/termux-app.svg?branch=master)](https://travis-ci.org/termux/termux-app)
[![Join the chat at https://gitter.im/termux/termux](https://badges.gitter.im/termux/termux.svg)](https://gitter.im/termux/termux) [![Join the chat at https://gitter.im/termux/termux](https://badges.gitter.im/termux/termux.svg)](https://gitter.im/termux/termux)
[Termux](https://termux.com) is an Android terminal app and Linux environment.
Termux is an Android terminal app and Linux environment.
* [Termux on Google Play Store](https://play.google.com/store/apps/details?id=com.termux) * [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 on F-Droid](https://f-droid.org/repository/browse/?fdid=com.termux)
* [termux.com](http://termux.com)
* [Termux Help](http://termux.com/help/) * [Termux Help](http://termux.com/help/)
* [Termux app on GitHub](https://github.com/termux/termux-app)
* [Termux packages on GitHub](https://github.com/termux/termux-packages)
* [Termux Google+ community](http://termux.com/community/) * [Termux Google+ community](http://termux.com/community/)
License 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)
=======
Released under [the GPLv3 license](https://www.gnu.org/licenses/gpl.html). Contains code from `Terminal Emulator for Android` which is released under [the Apache License 2.0](https://www.apache.org/licenses/).
Building JNI libraries
======================
Execute the `build-jnilibs.sh` script to build the required JNI libraries.
Terminal resources Terminal resources
================== ==================

View File

@@ -1,30 +1,27 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
android { android {
compileSdkVersion 23 compileSdkVersion 25
buildToolsVersion "23.0.2" buildToolsVersion "25.0.3"
dependencies { dependencies {
compile 'com.android.support:support-annotations:23.1.1' compile 'com.android.support:support-annotations:25.3.1'
} compile "com.android.support:support-v4:25.3.1"
compile project(":terminal-view")
sourceSets {
main {
jni.srcDirs = []
}
} }
defaultConfig { defaultConfig {
applicationId "com.termux" applicationId "com.termux"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 23 targetSdkVersion 25
versionCode 31 versionCode 50
versionName "0.31" versionName "0.50"
} }
buildTypes { buildTypes {
release { release {
minifyEnabled false minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
} }

View File

@@ -1,13 +0,0 @@
package com.termux;
import android.app.Application;
import android.test.ApplicationTestCase;
/**
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
*/
public class ApplicationTest extends ApplicationTestCase<Application> {
public ApplicationTest() {
super(Application.class);
}
}

View File

@@ -13,9 +13,11 @@
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<application <application
android:extractNativeLibs="true"
android:allowBackup="true" android:allowBackup="true"
android:fullBackupContent="@xml/backupscheme" android:fullBackupContent="@xml/backupscheme"
android:icon="@drawable/ic_launcher" android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:banner="@drawable/banner" android:banner="@drawable/banner"
android:label="@string/application_name" android:label="@string/application_name"
android:theme="@style/Theme.Termux" android:theme="@style/Theme.Termux"
@@ -34,6 +36,7 @@
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" /> <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter> </intent-filter>
<meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts" />
</activity> </activity>
<activity <activity
@@ -49,44 +52,53 @@
android:taskAffinity="com.termux.filereceiver" android:taskAffinity="com.termux.filereceiver"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:noHistory="true"> android:noHistory="true">
<!-- Accept multiple file types when sending. -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.EDIT" />
<action android:name="android.intent.action.SEND"/> <action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" /> <data android:mimeType="application/*" />
<data android:mimeType="audio/*" />
<data android:mimeType="image/*" />
<data android:mimeType="message/*" />
<data android:mimeType="multipart/*" />
<data android:mimeType="text/*" />
<data android:mimeType="video/*" />
</intent-filter> </intent-filter>
</activity> <!-- Be more restrictive for viewing files, restricting ourselves to text files. -->
<activity
android:name="com.termux.filepicker.TermuxFilePickerActivity"
android:label="@string/application_name"
android:theme="@android:style/Theme.Material.Light.DarkActionBar"
android:noHistory="true">
<intent-filter> <intent-filter>
<!-- <action android:name="android.intent.action.VIEW"/>
http://stackoverflow.com/questions/6486716/using-intent-action-pick-for-specific-path
"That said, you should consider ACTION_PICK deprecated. The modern action is ACTION_GET_CONTENT
which is much better supported; you will find support of ACTION_PICK spotty and inconsistent.
Unfortunately ACTION_GET_CONTENT also does not let you specify a directory."
-->
<action android:name="android.intent.action.GET_CONTENT" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.OPENABLE" /> <data android:mimeType="text/*" />
<data android:mimeType="*/*" /> <data android:mimeType="application/json" />
<data android:mimeType="application/*xml*" />
<data android:mimeType="application/*latex*" />
<data android:mimeType="application/javascript" />
</intent-filter> </intent-filter>
</activity> </activity>
<provider android:authorities="com.termux.filepicker.provider" <provider
android:readPermission="com.termux.filepickder.READ" android:name=".filepicker.TermuxDocumentsProvider"
android:exported="true" android:authorities="com.termux.documents"
android:grantUriPermissions="true" android:grantUriPermissions="true"
android:name="com.termux.filepicker.TermuxFilePickerProvider" /> android:exported="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
<service <service
android:name="com.termux.app.TermuxService" android:name="com.termux.app.TermuxService"
android:exported="false" /> android:exported="false" />
<receiver android:name=".app.TermuxOpenReceiver" />
<provider android:authorities="com.termux.files"
android:readPermission="android.permission.permRead"
android:exported="true"
android:grantUriPermissions="true"
android:name="com.termux.app.TermuxOpenReceiver$ContentProvider" />
</application> </application>
</manifest> </manifest>

View File

@@ -0,0 +1,193 @@
package com.termux.app;
import android.util.Log;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* A background job launched by Termux.
*/
public final class BackgroundJob {
private static final String LOG_TAG = "termux-task";
final Process mProcess;
public BackgroundJob(String cwd, String fileToExecute, final String[] args, final TermuxService service) {
String[] env = buildEnvironment(false, cwd);
if (cwd == null) cwd = TermuxService.HOME_PATH;
final String[] progArray = setupProcessArgs(fileToExecute, args);
final String processDescription = Arrays.toString(progArray);
Process process;
try {
process = Runtime.getRuntime().exec(progArray, env, new File(cwd));
} catch (IOException e) {
mProcess = null;
// TODO: Visible error message?
Log.e(LOG_TAG, "Failed running background job: " + processDescription, e);
return;
}
mProcess = process;
final int pid = getPid(mProcess);
new Thread() {
@Override
public void run() {
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);
}
} catch (IOException e) {
Log.e(LOG_TAG, "Error reading output", e);
}
try {
int exitCode = mProcess.waitFor();
service.onBackgroundJobExited(BackgroundJob.this);
if (exitCode == 0) {
Log.i(LOG_TAG, "[" + pid + "] exited normally");
} else {
Log.w(LOG_TAG, "[" + pid + "] exited with code: " + exitCode);
}
} catch (InterruptedException e) {
// 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.
}
}
};
}
public static String[] buildEnvironment(boolean failSafe, String cwd) {
new File(TermuxService.HOME_PATH).mkdirs();
if (cwd == null) cwd = TermuxService.HOME_PATH;
final String termEnv = "TERM=xterm-256color";
final String homeEnv = "HOME=" + TermuxService.HOME_PATH;
final String prefixEnv = "PREFIX=" + TermuxService.PREFIX_PATH;
final String androidRootEnv = "ANDROID_ROOT=" + System.getenv("ANDROID_ROOT");
final String androidDataEnv = "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.
final String externalStorageEnv = "EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE");
if (failSafe) {
// Keep the default path so that system binaries can be used in the failsafe session.
final String pathEnv = "PATH=" + System.getenv("PATH");
return new String[]{termEnv, homeEnv, prefixEnv, androidRootEnv, androidDataEnv, pathEnv, externalStorageEnv};
} else {
final String ps1Env = "PS1=$ ";
final String ldEnv = "LD_LIBRARY_PATH=" + TermuxService.PREFIX_PATH + "/lib";
final String langEnv = "LANG=en_US.UTF-8";
final String pathEnv = "PATH=" + TermuxService.PREFIX_PATH + "/bin:" + TermuxService.PREFIX_PATH + "/bin/applets";
final String pwdEnv = "PWD=" + cwd;
final String tmpdirEnv = "TMPDIR=" + TermuxService.PREFIX_PATH + "/tmp";
return new String[]{termEnv, homeEnv, prefixEnv, ps1Env, ldEnv, langEnv, pathEnv, pwdEnv, androidRootEnv, androidDataEnv, externalStorageEnv, tmpdirEnv};
}
}
public static int getPid(Process p) {
try {
Field f = p.getClass().getDeclaredField("pid");
f.setAccessible(true);
try {
return f.getInt(p);
} finally {
f.setAccessible(false);
}
} catch (Throwable e) {
return -1;
}
}
static String[] setupProcessArgs(String fileToExecute, String[] args) {
// The file to execute may either be:
// - An elf file, in which we execute it directly.
// - A script file without shebang, which we execute with our standard shell $PREFIX/bin/sh instead of the
// system /system/bin/sh. The system shell may vary and may not work at all due to LD_LIBRARY_PATH.
// - A file with shebang, which we try to handle with e.g. /bin/foo -> $PREFIX/bin/foo.
String interpreter = null;
try {
File file = new File(fileToExecute);
try (FileInputStream in = new FileInputStream(file)) {
byte[] buffer = new byte[256];
int bytesRead = in.read(buffer);
if (bytesRead > 4) {
if (buffer[0] == 0x7F && buffer[1] == 'E' && buffer[2] == 'L' && buffer[3] == 'F') {
// Elf file, do nothing.
} else if (buffer[0] == '#' && buffer[1] == '!') {
// Try to parse shebang.
StringBuilder builder = new StringBuilder();
for (int i = 2; i < bytesRead; i++) {
char c = (char) buffer[i];
if (c == ' ' || c == '\n') {
if (builder.length() == 0) {
// Skip whitespace after shebang.
} else {
// End of shebang.
String executable = builder.toString();
if (executable.startsWith("/usr") || executable.startsWith("/bin")) {
String[] parts = executable.split("/");
String binary = parts[parts.length - 1];
interpreter = TermuxService.PREFIX_PATH + "/bin/" + binary;
}
break;
}
} else {
builder.append(c);
}
}
} else {
// No shebang and no ELF, use standard shell.
interpreter = TermuxService.PREFIX_PATH + "/bin/sh";
}
}
}
} catch (IOException e) {
// Ignore.
}
List<String> result = new ArrayList<>();
if (interpreter != null) result.add(interpreter);
result.add(fileToExecute);
if (args != null) Collections.addAll(result, args);
return result.toArray(new String[result.size()]);
}
}

View File

@@ -13,52 +13,52 @@ import android.widget.TextView;
public final class DialogUtils { public final class DialogUtils {
public interface TextSetListener { public interface TextSetListener {
void onTextSet(String text); void onTextSet(String text);
} }
public static void textInput(Activity activity, int titleText, String initialText, public static void textInput(Activity activity, int titleText, String initialText,
int positiveButtonText, final TextSetListener onPositive, int positiveButtonText, final TextSetListener onPositive,
int neutralButtonText, final TextSetListener onNeutral, int neutralButtonText, final TextSetListener onNeutral,
int negativeButtonText, final TextSetListener onNegative, int negativeButtonText, final TextSetListener onNegative,
final DialogInterface.OnDismissListener onDismiss) { final DialogInterface.OnDismissListener onDismiss) {
final EditText input = new EditText(activity); final EditText input = new EditText(activity);
input.setSingleLine(); input.setSingleLine();
if (initialText != null) { if (initialText != null) {
input.setText(initialText); input.setText(initialText);
Selection.setSelection(input.getText(), initialText.length()); Selection.setSelection(input.getText(), initialText.length());
} }
final AlertDialog[] dialogHolder = new AlertDialog[1]; final AlertDialog[] dialogHolder = new AlertDialog[1];
input.setImeActionLabel(activity.getResources().getString(positiveButtonText), KeyEvent.KEYCODE_ENTER); input.setImeActionLabel(activity.getResources().getString(positiveButtonText), KeyEvent.KEYCODE_ENTER);
input.setOnEditorActionListener(new TextView.OnEditorActionListener() { input.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override @Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
onPositive.onTextSet(input.getText().toString()); onPositive.onTextSet(input.getText().toString());
dialogHolder[0].dismiss(); dialogHolder[0].dismiss();
return true; return true;
} }
}); });
float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, activity.getResources().getDisplayMetrics()); float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, activity.getResources().getDisplayMetrics());
// https://www.google.com/design/spec/components/dialogs.html#dialogs-specs // https://www.google.com/design/spec/components/dialogs.html#dialogs-specs
int paddingTopAndSides = Math.round(16 * dipInPixels); int paddingTopAndSides = Math.round(16 * dipInPixels);
int paddingBottom = Math.round(24 * dipInPixels); int paddingBottom = Math.round(24 * dipInPixels);
LinearLayout layout = new LinearLayout(activity); LinearLayout layout = new LinearLayout(activity);
layout.setOrientation(LinearLayout.VERTICAL); layout.setOrientation(LinearLayout.VERTICAL);
layout.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); layout.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
layout.setPadding(paddingTopAndSides, paddingTopAndSides, paddingTopAndSides, paddingBottom); layout.setPadding(paddingTopAndSides, paddingTopAndSides, paddingTopAndSides, paddingBottom);
layout.addView(input); layout.addView(input);
AlertDialog.Builder builder = new AlertDialog.Builder(activity) AlertDialog.Builder builder = new AlertDialog.Builder(activity)
.setTitle(titleText).setView(layout) .setTitle(titleText).setView(layout)
.setPositiveButton(positiveButtonText, new DialogInterface.OnClickListener() { .setPositiveButton(positiveButtonText, new DialogInterface.OnClickListener() {
@Override @Override
public void onClick(DialogInterface d, int whichButton) { public void onClick(DialogInterface d, int whichButton) {
onPositive.onTextSet(input.getText().toString()); onPositive.onTextSet(input.getText().toString());
} }
}); });
if (onNeutral != null) { if (onNeutral != null) {
builder.setNeutralButton(neutralButtonText, new DialogInterface.OnClickListener() { builder.setNeutralButton(neutralButtonText, new DialogInterface.OnClickListener() {
@@ -82,9 +82,9 @@ public final class DialogUtils {
if (onDismiss != null) builder.setOnDismissListener(onDismiss); if (onDismiss != null) builder.setOnDismissListener(onDismiss);
dialogHolder[0] = builder.create(); dialogHolder[0] = builder.create();
dialogHolder[0].setCanceledOnTouchOutside(false); dialogHolder[0].setCanceledOnTouchOutside(false);
dialogHolder[0].show(); dialogHolder[0].show();
} }
} }

View File

@@ -0,0 +1,179 @@
package com.termux.app;
import android.content.Context;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
import android.view.KeyEvent;
import android.view.View;
import android.widget.Button;
import android.widget.GridLayout;
import android.widget.ToggleButton;
import com.termux.R;
import com.termux.terminal.TerminalSession;
import com.termux.view.TerminalView;
/**
* A view showing extra keys (such as Escape, Ctrl, Alt) not normally available on an Android soft
* keyboard.
*/
public final class ExtraKeysView extends GridLayout {
private static final int TEXT_COLOR = 0xFFFFFFFF;
public ExtraKeysView(Context context, AttributeSet attrs) {
super(context, attrs);
reload();
}
static void sendKey(View view, String keyName) {
int keyCode = 0;
String chars = null;
switch (keyName) {
case "ESC":
keyCode = KeyEvent.KEYCODE_ESCAPE;
break;
case "TAB":
keyCode = KeyEvent.KEYCODE_TAB;
break;
case "":
keyCode = KeyEvent.KEYCODE_DPAD_UP;
break;
case "":
keyCode = KeyEvent.KEYCODE_DPAD_LEFT;
break;
case "":
keyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
break;
case "":
keyCode = KeyEvent.KEYCODE_DPAD_DOWN;
break;
case "":
chars = "-";
break;
default:
chars = keyName;
}
if (keyCode > 0) {
view.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
view.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
} else {
TerminalView terminalView = (TerminalView) view.findViewById(R.id.terminal_view);
TerminalSession session = terminalView.getCurrentSession();
if (session != null) session.write(chars);
}
}
private ToggleButton controlButton;
private ToggleButton altButton;
private ToggleButton fnButton;
public boolean readControlButton() {
if (controlButton.isPressed()) return true;
boolean result = controlButton.isChecked();
if (result) {
controlButton.setChecked(false);
controlButton.setTextColor(TEXT_COLOR);
}
return result;
}
public boolean readAltButton() {
if (altButton.isPressed()) return true;
boolean result = altButton.isChecked();
if (result) {
altButton.setChecked(false);
altButton.setTextColor(TEXT_COLOR);
}
return result;
}
public boolean readFnButton() {
if (fnButton.isPressed()) return true;
boolean result = fnButton.isChecked();
if (result) {
fnButton.setChecked(false);
fnButton.setTextColor(TEXT_COLOR);
}
return result;
}
void reload() {
altButton = controlButton = null;
removeAllViews();
String[][] buttons = {
{"ESC", "CTRL", "ALT", "TAB", "", "/", "|"}
};
final int rows = buttons.length;
final int cols = buttons[0].length;
setRowCount(rows);
setColumnCount(cols);
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
final String buttonText = buttons[row][col];
Button button;
switch (buttonText) {
case "CTRL":
button = controlButton = new ToggleButton(getContext(), null, android.R.attr.buttonBarButtonStyle);
button.setClickable(true);
break;
case "ALT":
button = altButton = new ToggleButton(getContext(), null, android.R.attr.buttonBarButtonStyle);
button.setClickable(true);
break;
case "FN":
button = fnButton = new ToggleButton(getContext(), null, android.R.attr.buttonBarButtonStyle);
button.setClickable(true);
break;
default:
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
break;
}
button.setText(buttonText);
button.setTextColor(TEXT_COLOR);
final Button finalButton = button;
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
finalButton.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
View root = getRootView();
switch (buttonText) {
case "CTRL":
case "ALT":
case "FN":
ToggleButton self = (ToggleButton) finalButton;
self.setChecked(self.isChecked());
self.setTextColor(self.isChecked() ? 0xFF80DEEA : TEXT_COLOR);
break;
default:
sendKey(root, buttonText);
break;
}
}
});
GridLayout.LayoutParams param = new GridLayout.LayoutParams();
param.height = param.width = 0;
param.rightMargin = param.topMargin = 0;
param.setGravity(Gravity.LEFT);
float weight = "▲▼◀▶".contains(buttonText) ? 0.7f : 1.f;
param.columnSpec = GridLayout.spec(col, GridLayout.FILL, weight);
param.rowSpec = GridLayout.spec(row, GridLayout.FILL, 1.f);
button.setLayoutParams(param);
addView(button);
}
}
}
}

View File

@@ -1,68 +1,67 @@
package com.termux.app; package com.termux.app;
import android.app.Activity; import android.graphics.Color;
import android.graphics.Rect; import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.view.View; import android.view.View;
import android.view.ViewTreeObserver;
import android.view.Window; import com.termux.R;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.FrameLayout.LayoutParams;
/** /**
* Utility to make the touch keyboard and immersive mode work with full screen activities. * Utility to manage full screen immersive mode.
* * <p/>
* See https://code.google.com/p/android/issues/detail?id=5497 * See https://code.google.com/p/android/issues/detail?id=5497
*/ */
final class FullScreenHelper implements ViewTreeObserver.OnGlobalLayoutListener { final class FullScreenHelper {
private boolean mEnabled = false; private boolean mEnabled = false;
private final Activity mActivity; final TermuxActivity mActivity;
private final Rect mWindowRect = new Rect();
public FullScreenHelper(Activity activity) { public FullScreenHelper(TermuxActivity activity) {
this.mActivity = activity; this.mActivity = activity;
} }
public void setImmersive(boolean enabled) { public void setImmersive(boolean enabled) {
Window win = mActivity.getWindow(); if (enabled == mEnabled) return;
mEnabled = enabled;
if (enabled == mEnabled) { View decorView = mActivity.getWindow().getDecorView();
if (!enabled) win.setFlags(0, WindowManager.LayoutParams.FLAG_FULLSCREEN);
return;
}
mEnabled = enabled;
final View childViewOfContent = ((FrameLayout) mActivity.findViewById(android.R.id.content)).getChildAt(0); if (enabled) {
if (enabled) { decorView.setOnSystemUiVisibilityChangeListener
win.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); (new View.OnSystemUiVisibilityChangeListener() {
setImmersiveMode(); @Override
childViewOfContent.getViewTreeObserver().addOnGlobalLayoutListener(this); public void onSystemUiVisibilityChange(int visibility) {
} else { if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
win.setFlags(0, WindowManager.LayoutParams.FLAG_FULLSCREEN); if (mActivity.mSettings.isShowExtraKeys()) {
win.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); mActivity.findViewById(R.id.viewpager).setVisibility(View.VISIBLE);
childViewOfContent.getViewTreeObserver().removeOnGlobalLayoutListener(this); }
((LayoutParams) childViewOfContent.getLayoutParams()).height = android.view.ViewGroup.LayoutParams.MATCH_PARENT; setImmersiveMode();
} } else {
} mActivity.findViewById(R.id.viewpager).setVisibility(View.GONE);
}
}
});
setImmersiveMode();
} else {
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
decorView.setOnSystemUiVisibilityChangeListener(null);
}
}
private void setImmersiveMode() { private static boolean isColorLight(int color) {
mActivity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); double darkness = 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255;
} return darkness < 0.5;
}
@Override void setImmersiveMode() {
public void onGlobalLayout() { int flags = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
final View childViewOfContent = ((FrameLayout) mActivity.findViewById(android.R.id.content)).getChildAt(0); | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_FULLSCREEN;
if (mEnabled) setImmersiveMode(); int color = ((ColorDrawable) mActivity.getWindow().getDecorView().getBackground()).getColor();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && isColorLight(color))
childViewOfContent.getWindowVisibleDisplayFrame(mWindowRect); flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
int usableHeightNow = Math.min(mWindowRect.height(), childViewOfContent.getRootView().getHeight()); mActivity.getWindow().getDecorView().setSystemUiVisibility(flags);
FrameLayout.LayoutParams layout = (LayoutParams) childViewOfContent.getLayoutParams(); }
if (layout.height != usableHeightNow) {
layout.height = usableHeightNow;
childViewOfContent.requestLayout();
}
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -15,61 +15,61 @@ import android.widget.RelativeLayout;
/** Basic embedded browser for viewing help pages. */ /** Basic embedded browser for viewing help pages. */
public final class TermuxHelpActivity extends Activity { public final class TermuxHelpActivity extends Activity {
private WebView mWebView; WebView mWebView;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
final RelativeLayout progressLayout = new RelativeLayout(this); final RelativeLayout progressLayout = new RelativeLayout(this);
RelativeLayout.LayoutParams lParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); RelativeLayout.LayoutParams lParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
lParams.addRule(RelativeLayout.CENTER_IN_PARENT); lParams.addRule(RelativeLayout.CENTER_IN_PARENT);
ProgressBar progressBar = new ProgressBar(this); ProgressBar progressBar = new ProgressBar(this);
progressBar.setIndeterminate(true); progressBar.setIndeterminate(true);
progressBar.setLayoutParams(lParams); progressBar.setLayoutParams(lParams);
progressLayout.addView(progressBar); progressLayout.addView(progressBar);
mWebView = new WebView(this); mWebView = new WebView(this);
WebSettings settings = mWebView.getSettings(); WebSettings settings = mWebView.getSettings();
settings.setCacheMode(WebSettings.LOAD_NO_CACHE); settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
settings.setAppCacheEnabled(false); settings.setAppCacheEnabled(false);
setContentView(progressLayout); setContentView(progressLayout);
mWebView.clearCache(true); mWebView.clearCache(true);
mWebView.setWebViewClient(new WebViewClient() { mWebView.setWebViewClient(new WebViewClient() {
@Override @Override
public boolean shouldOverrideUrlLoading(WebView view, String url) { public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.startsWith("https://termux.com")) { if (url.startsWith("https://termux.com")) {
// Inline help. // Inline help.
setContentView(progressLayout); setContentView(progressLayout);
return false; return false;
} }
try { try {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
} catch (ActivityNotFoundException e) { } catch (ActivityNotFoundException e) {
// Android TV does not have a system browser. // Android TV does not have a system browser.
setContentView(progressLayout); setContentView(progressLayout);
return false; return false;
} }
return true; return true;
} }
@Override @Override
public void onPageFinished(WebView view, String url) { public void onPageFinished(WebView view, String url) {
setContentView(mWebView); setContentView(mWebView);
} }
}); });
mWebView.loadUrl("https://termux.com/help.html"); mWebView.loadUrl("https://termux.com/help.html");
} }
@Override @Override
public void onBackPressed() { public void onBackPressed() {
if (mWebView.canGoBack()) { if (mWebView.canGoBack()) {
mWebView.goBack(); mWebView.goBack();
} else { } else {
super.onBackPressed(); super.onBackPressed();
} }
} }
} }

View File

@@ -7,7 +7,9 @@ import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener; import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface.OnDismissListener; import android.content.DialogInterface.OnDismissListener;
import android.os.Build;
import android.os.Environment; import android.os.Environment;
import android.os.UserManager;
import android.system.Os; import android.system.Os;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
@@ -23,231 +25,251 @@ import java.io.InputStreamReader;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream; import java.util.zip.ZipInputStream;
/** /**
* Install the Termux bootstrap packages if necessary by following the below steps: * Install the Termux bootstrap packages if necessary by following the below steps:
* * <p/>
* (1) If $PREFIX already exist, assume that it is correct and be done. Note that this relies on that we do not create a * (1) If $PREFIX already exist, assume that it is correct and be done. Note that this relies on that we do not create a
* broken $PREFIX folder below. * broken $PREFIX folder below.
* * <p/>
* (2) A progress dialog is shown with "Installing..." message and a spinner. * (2) A progress dialog is shown with "Installing..." message and a spinner.
* * <p/>
* (3) A staging folder, $STAGING_PREFIX, is {@link #deleteFolder(File)} if left over from broken installation below. * (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 architecture is determined and an appropriate bootstrap zip url is determined in {@link #determineZipUrl()}.
* * <p/>
* (5) The zip, containing entries relative to the $PREFIX, is is downloaded and extracted by a zip input stream * (5) The zip, containing entries relative to the $PREFIX, is is downloaded and extracted by a zip input stream
* continously encountering zip file entries: * continously encountering zip file entries:
* * <p/>
* (5.1) If the zip entry encountered is SYMLINKS.txt, go through it and remember all symlinks to setup. * (5.1) If the zip entry encountered is SYMLINKS.txt, go through it and remember all symlinks to setup.
* * <p/>
* (5.2) For every other zip entry, extract it into $STAGING_PREFIX and set execute permissions if necessary. * (5.2) For every other zip entry, extract it into $STAGING_PREFIX and set execute permissions if necessary.
*/ */
final class TermuxInstaller { final class TermuxInstaller {
/** Performs setup if necessary. */ /** Performs setup if necessary. */
static void setupIfNeeded(final Activity activity, final Runnable whenDone) { static void setupIfNeeded(final Activity activity, final Runnable whenDone) {
// Termux can only be run as the primary user (device owner) since only that // Termux can only be run as the primary user (device owner) since only that
// account has the expected file system paths. Verify that: // account has the expected file system paths. Verify that:
android.os.UserManager um = (android.os.UserManager) activity.getSystemService(Context.USER_SERVICE); UserManager um = (UserManager) activity.getSystemService(Context.USER_SERVICE);
boolean isPrimaryUser = um.getSerialNumberForUser(android.os.Process.myUserHandle()) == 0; boolean isPrimaryUser = um.getSerialNumberForUser(android.os.Process.myUserHandle()) == 0;
if (!isPrimaryUser) { if (!isPrimaryUser) {
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_not_primary_user_message) new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_not_primary_user_message)
.setOnDismissListener(new OnDismissListener() { .setOnDismissListener(new OnDismissListener() {
@Override @Override
public void onDismiss(DialogInterface dialog) { public void onDismiss(DialogInterface dialog) {
System.exit(0); System.exit(0);
} }
}).setPositiveButton(android.R.string.ok, null).show(); }).setPositiveButton(android.R.string.ok, null).show();
return; return;
} }
final File PREFIX_FILE = new File(TermuxService.PREFIX_PATH); final File PREFIX_FILE = new File(TermuxService.PREFIX_PATH);
if (PREFIX_FILE.isDirectory()) { if (PREFIX_FILE.isDirectory()) {
whenDone.run(); whenDone.run();
return; return;
} }
final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false); final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false);
new Thread() { new Thread() {
@Override @Override
public void run() { public void run() {
try { try {
final String STAGING_PREFIX_PATH = TermuxService.FILES_PATH + "/usr-staging"; final String STAGING_PREFIX_PATH = TermuxService.FILES_PATH + "/usr-staging";
final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH); final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH);
if (STAGING_PREFIX_FILE.exists()) { if (STAGING_PREFIX_FILE.exists()) {
deleteFolder(STAGING_PREFIX_FILE); deleteFolder(STAGING_PREFIX_FILE);
} }
final byte[] buffer = new byte[8096]; final byte[] buffer = new byte[8096];
final List<Pair<String, String>> symlinks = new ArrayList<>(50); final List<Pair<String, String>> symlinks = new ArrayList<>(50);
final URL zipUrl = determineZipUrl(); final URL zipUrl = determineZipUrl();
try (ZipInputStream zipInput = new ZipInputStream(zipUrl.openStream())) { try (ZipInputStream zipInput = new ZipInputStream(zipUrl.openStream())) {
ZipEntry zipEntry; ZipEntry zipEntry;
while ((zipEntry = zipInput.getNextEntry()) != null) { while ((zipEntry = zipInput.getNextEntry()) != null) {
if (zipEntry.getName().equals("SYMLINKS.txt")) { if (zipEntry.getName().equals("SYMLINKS.txt")) {
BufferedReader symlinksReader = new BufferedReader(new InputStreamReader(zipInput)); BufferedReader symlinksReader = new BufferedReader(new InputStreamReader(zipInput));
String line; String line;
while ((line = symlinksReader.readLine()) != null) { while ((line = symlinksReader.readLine()) != null) {
String[] parts = line.split(""); String[] parts = line.split("");
if (parts.length != 2) throw new RuntimeException("Malformed symlink line: " + line); if (parts.length != 2)
String oldPath = parts[0]; throw new RuntimeException("Malformed symlink line: " + line);
String newPath = STAGING_PREFIX_PATH + "/" + parts[1]; String oldPath = parts[0];
symlinks.add(Pair.create(oldPath, newPath)); String newPath = STAGING_PREFIX_PATH + "/" + parts[1];
} symlinks.add(Pair.create(oldPath, newPath));
} else { }
String zipEntryName = zipEntry.getName(); } else {
File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName); String zipEntryName = zipEntry.getName();
if (zipEntry.isDirectory()) { File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName);
if (!targetFile.mkdirs()) throw new RuntimeException("Failed to create directory: " + targetFile.getAbsolutePath()); if (zipEntry.isDirectory()) {
} else { if (!targetFile.mkdirs())
try (FileOutputStream outStream = new FileOutputStream(targetFile)) { throw new RuntimeException("Failed to create directory: " + targetFile.getAbsolutePath());
int readBytes; } else {
while ((readBytes = zipInput.read(buffer)) != -1) try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
outStream.write(buffer, 0, readBytes); int readBytes;
} while ((readBytes = zipInput.read(buffer)) != -1)
if (zipEntryName.startsWith("bin/") || zipEntryName.startsWith("libexec") || zipEntryName.startsWith("lib/apt/methods")) { outStream.write(buffer, 0, readBytes);
//noinspection OctalInteger }
Os.chmod(targetFile.getAbsolutePath(), 0700); if (zipEntryName.startsWith("bin/") || zipEntryName.startsWith("libexec") || zipEntryName.startsWith("lib/apt/methods")) {
} //noinspection OctalInteger
} Os.chmod(targetFile.getAbsolutePath(), 0700);
} }
} }
} }
}
}
if (symlinks.isEmpty()) throw new RuntimeException("No SYMLINKS.txt encountered"); if (symlinks.isEmpty())
for (Pair<String, String> symlink : symlinks) { throw new RuntimeException("No SYMLINKS.txt encountered");
Os.symlink(symlink.first, symlink.second); for (Pair<String, String> symlink : symlinks) {
} Os.symlink(symlink.first, symlink.second);
}
if (!STAGING_PREFIX_FILE.renameTo(PREFIX_FILE)) { if (!STAGING_PREFIX_FILE.renameTo(PREFIX_FILE)) {
throw new RuntimeException("Unable to rename staging folder"); throw new RuntimeException("Unable to rename staging folder");
} }
activity.runOnUiThread(new Runnable() { activity.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
whenDone.run(); whenDone.run();
} }
}); });
} catch (final Exception e) { } catch (final Exception e) {
Log.e(EmulatorDebug.LOG_TAG, "Bootstrap error", e); Log.e(EmulatorDebug.LOG_TAG, "Bootstrap error", e);
activity.runOnUiThread(new Runnable() { activity.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
try { try {
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body) new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
.setNegativeButton(R.string.bootstrap_error_abort, new OnClickListener() { .setNegativeButton(R.string.bootstrap_error_abort, new OnClickListener() {
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
dialog.dismiss(); dialog.dismiss();
activity.finish(); activity.finish();
} }
}).setPositiveButton(R.string.bootstrap_error_try_again, new OnClickListener() { }).setPositiveButton(R.string.bootstrap_error_try_again, new OnClickListener() {
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
dialog.dismiss(); dialog.dismiss();
TermuxInstaller.setupIfNeeded(activity, whenDone); TermuxInstaller.setupIfNeeded(activity, whenDone);
} }
}).show(); }).show();
} catch (WindowManager.BadTokenException e) { } catch (WindowManager.BadTokenException e) {
// Activity already dismissed - ignore. // Activity already dismissed - ignore.
} }
} }
}); });
} finally { } finally {
activity.runOnUiThread(new Runnable() { activity.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
try { try {
progress.dismiss(); progress.dismiss();
} catch (RuntimeException e) { } catch (RuntimeException e) {
// Activity already dismissed - ignore. // Activity already dismissed - ignore.
} }
} }
}); });
} }
} }
}.start(); }.start();
} }
/** Get bootstrap zip url for this systems cpu architecture. */ /** Get bootstrap zip url for this systems cpu architecture. */
static URL determineZipUrl() throws MalformedURLException { static URL determineZipUrl() throws MalformedURLException {
String arch = System.getProperty("os.arch"); String archName = determineTermuxArchName();
if (arch.startsWith("armv8")) { return new URL("https://termux.net/bootstrap/bootstrap-" + archName + ".zip");
arch = "aarch64";
} else if (arch.startsWith("arm")) {
// Handle different arm variants such as armv7l:
arch = "arm";
} else if (arch.startsWith("x86")) { // "x86" on arcwelder, "x86_64" on 64-bit android.
arch = "i686";
}
return new URL("https://termux.net/bootstrap/bootstrap-" + arch + ".zip");
} }
/** Delete a folder and all its content or throw. */ private static String determineTermuxArchName() {
static void deleteFolder(File fileOrDirectory) { // Note that we cannot use System.getProperty("os.arch") since that may give e.g. "aarch64"
File[] children = fileOrDirectory.listFiles(); // while a 64-bit runtime may not be installed (like on the Samsung Galaxy S5 Neo).
if (children != null) { // Instead we search through the supported abi:s on the device, see:
for (File child : children) { // http://developer.android.com/ndk/guides/abis.html
deleteFolder(child); // 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.
if (!fileOrDirectory.delete()) { for (String androidArch : Build.SUPPORTED_ABIS) {
throw new RuntimeException("Unable to delete " + (fileOrDirectory.isDirectory() ? "directory " : "file ") + fileOrDirectory.getAbsolutePath()); 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 void setupStorageSymlinks(final Context context) { /** Delete a folder and all its content or throw. */
final String LOG_TAG = "termux-storage"; static void deleteFolder(File fileOrDirectory) {
new Thread() { File[] children = fileOrDirectory.listFiles();
public void run() { if (children != null) {
try { for (File child : children) {
File storageDir = new File(TermuxService.HOME_PATH, "storage"); deleteFolder(child);
}
}
if (!fileOrDirectory.delete()) {
throw new RuntimeException("Unable to delete " + (fileOrDirectory.isDirectory() ? "directory " : "file ") + fileOrDirectory.getAbsolutePath());
}
}
if (storageDir.exists() && !storageDir.delete()) { public static void setupStorageSymlinks(final Context context) {
Log.e(LOG_TAG, "Could not delete old $HOME/storage"); final String LOG_TAG = "termux-storage";
return; new Thread() {
} public void run() {
try {
File storageDir = new File(TermuxService.HOME_PATH, "storage");
if (!storageDir.mkdirs()) { if (storageDir.exists() && !storageDir.delete()) {
Log.e(LOG_TAG, "Unable to mkdirs() for $HOME/storage"); Log.e(LOG_TAG, "Could not delete old $HOME/storage");
return; return;
} }
File sharedDir = Environment.getExternalStorageDirectory(); if (!storageDir.mkdirs()) {
Os.symlink(sharedDir.getAbsolutePath(), new File(storageDir, "shared").getAbsolutePath()); Log.e(LOG_TAG, "Unable to mkdirs() for $HOME/storage");
return;
}
File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); File sharedDir = Environment.getExternalStorageDirectory();
Os.symlink(downloadsDir.getAbsolutePath(), new File(storageDir, "downloads").getAbsolutePath()); Os.symlink(sharedDir.getAbsolutePath(), new File(storageDir, "shared").getAbsolutePath());
File dcimDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM); File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
Os.symlink(dcimDir.getAbsolutePath(), new File(storageDir, "dcim").getAbsolutePath()); Os.symlink(downloadsDir.getAbsolutePath(), new File(storageDir, "downloads").getAbsolutePath());
File picturesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); File dcimDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
Os.symlink(picturesDir.getAbsolutePath(), new File(storageDir, "pictures").getAbsolutePath()); Os.symlink(dcimDir.getAbsolutePath(), new File(storageDir, "dcim").getAbsolutePath());
File musicDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC); File picturesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
Os.symlink(musicDir.getAbsolutePath(), new File(storageDir, "music").getAbsolutePath()); Os.symlink(picturesDir.getAbsolutePath(), new File(storageDir, "pictures").getAbsolutePath());
File moviesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES); File musicDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC);
Os.symlink(moviesDir.getAbsolutePath(), new File(storageDir, "movies").getAbsolutePath()); Os.symlink(musicDir.getAbsolutePath(), new File(storageDir, "music").getAbsolutePath());
final File[] dirs = context.getExternalFilesDirs(null); File moviesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
if (dirs != null && dirs.length >= 2) { Os.symlink(moviesDir.getAbsolutePath(), new File(storageDir, "movies").getAbsolutePath());
final File externalDir = dirs[1];
Os.symlink(externalDir.getAbsolutePath(), new File(storageDir, "external").getAbsolutePath()); final File[] dirs = context.getExternalFilesDirs(null);
} if (dirs != null && dirs.length > 1) {
} catch (Exception e) { for (int i = 1; i < dirs.length; i++) {
Log.e(LOG_TAG, "Error setting up link", e); File dir = dirs[i];
} if (dir == null) continue;
} String symlinkName = "external-" + i;
}.start(); Os.symlink(dir.getAbsolutePath(), new File(storageDir, symlinkName).getAbsolutePath());
} }
}
} catch (Exception e) {
Log.e(LOG_TAG, "Error setting up link", e);
}
}
}.start();
}
} }

View File

@@ -0,0 +1,178 @@
package com.termux.app;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
import android.support.annotation.NonNull;
import android.util.Log;
import android.webkit.MimeTypeMap;
import com.termux.terminal.EmulatorDebug;
import java.io.File;
import java.io.FileNotFoundException;
public class TermuxOpenReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
final Uri data = intent.getData();
if (data == null) {
Log.e(EmulatorDebug.LOG_TAG, "termux-open: Called without intent data");
return;
}
final String filePath = data.getPath();
final String contentTypeExtra = intent.getStringExtra("content-type");
final boolean useChooser = intent.getBooleanExtra("chooser", false);
final String intentAction = intent.getAction() == null ? Intent.ACTION_VIEW : intent.getAction();
switch (intentAction) {
case Intent.ACTION_SEND:
case Intent.ACTION_VIEW:
// Ok.
break;
default:
Log.e(EmulatorDebug.LOG_TAG, "Invalid action '" + intentAction + "', using 'view'");
break;
}
final boolean isExternalUrl = data.getScheme() != null && !data.getScheme().equals("file");
if (isExternalUrl) {
Intent urlIntent = new Intent(intentAction, data);
if (intentAction.equals(Intent.ACTION_SEND)) {
urlIntent.putExtra(Intent.EXTRA_TEXT, data.toString());
urlIntent.setData(null);
} else if (contentTypeExtra != null) {
urlIntent.setDataAndType(data, contentTypeExtra);
}
urlIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
context.startActivity(urlIntent);
} catch (ActivityNotFoundException e) {
Log.e(EmulatorDebug.LOG_TAG, "termux-open: No app handles the url " + data);
}
return;
}
final File fileToShare = new File(filePath);
if (!(fileToShare.isFile() && fileToShare.canRead())) {
Log.e(EmulatorDebug.LOG_TAG, "termux-open: Not a readable file: '" + fileToShare.getAbsolutePath() + "'");
return;
}
Intent sendIntent = new Intent();
sendIntent.setAction(intentAction);
sendIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION);
String contentTypeToUse;
if (contentTypeExtra == null) {
String fileName = fileToShare.getName();
int lastDotIndex = fileName.lastIndexOf('.');
String fileExtension = fileName.substring(lastDotIndex + 1, fileName.length());
MimeTypeMap mimeTypes = MimeTypeMap.getSingleton();
// Lower casing makes it work with e.g. "JPG":
contentTypeToUse = mimeTypes.getMimeTypeFromExtension(fileExtension.toLowerCase());
if (contentTypeToUse == null) contentTypeToUse = "application/octet-stream";
} else {
contentTypeToUse = contentTypeExtra;
}
Uri uriToShare = Uri.withAppendedPath(Uri.parse("content://com.termux.files/"), filePath);
if (Intent.ACTION_SEND.equals(intentAction)) {
sendIntent.putExtra(Intent.EXTRA_STREAM, uriToShare);
sendIntent.setType(contentTypeToUse);
} else {
sendIntent.setDataAndType(uriToShare, contentTypeToUse);
}
if (useChooser) {
sendIntent = Intent.createChooser(sendIntent, null).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
try {
context.startActivity(sendIntent);
} catch (ActivityNotFoundException e) {
Log.e(EmulatorDebug.LOG_TAG, "termux-open: No app handles the url " + data);
}
}
public static class ContentProvider extends android.content.ContentProvider {
@Override
public boolean onCreate() {
return true;
}
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
File file = new File(uri.getPath());
if (projection == null) {
projection = new String[]{
MediaStore.MediaColumns.DISPLAY_NAME,
MediaStore.MediaColumns.SIZE,
MediaStore.MediaColumns._ID
};
}
Object[] row = new Object[projection.length];
for (int i = 0; i < projection.length; i++) {
String column = projection[i];
Object value;
switch (column) {
case MediaStore.MediaColumns.DISPLAY_NAME:
value = file.getName();
break;
case MediaStore.MediaColumns.SIZE:
value = (int) file.length();
break;
case MediaStore.MediaColumns._ID:
value = 1;
break;
default:
value = null;
}
row[i] = value;
}
MatrixCursor cursor = new MatrixCursor(projection);
cursor.addRow(row);
return cursor;
}
@Override
public String getType(@NonNull Uri uri) {
return null;
}
@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {
return null;
}
@Override
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
return 0;
}
@Override
public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
File file = new File(uri.getPath());
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
}
}

View File

@@ -14,129 +14,185 @@ import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties; import java.util.Properties;
final class TermuxPreferences { final class TermuxPreferences {
@IntDef({BELL_VIBRATE, BELL_BEEP, BELL_IGNORE}) @IntDef({BELL_VIBRATE, BELL_BEEP, BELL_IGNORE})
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
public @interface AsciiBellBehaviour {} public @interface AsciiBellBehaviour {
}
static final int BELL_VIBRATE = 1; static final int BELL_VIBRATE = 1;
static final int BELL_BEEP = 2; static final int BELL_BEEP = 2;
static final int BELL_IGNORE = 3; static final int BELL_IGNORE = 3;
private final int MIN_FONTSIZE; private final int MIN_FONTSIZE;
private static final int MAX_FONTSIZE = 256; private static final int MAX_FONTSIZE = 256;
private static final String FULLSCREEN_KEY = "fullscreen"; private static final String FULLSCREEN_KEY = "fullscreen";
private static final String FONTSIZE_KEY = "fontsize"; private static final String SHOW_EXTRA_KEYS_KEY = "show_extra_keys";
private static final String CURRENT_SESSION_KEY = "current_session"; private static final String FONTSIZE_KEY = "fontsize";
private static final String SHOW_WELCOME_DIALOG_KEY = "intro_dialog"; private static final String CURRENT_SESSION_KEY = "current_session";
private boolean mFullScreen; private boolean mFullScreen;
private int mFontSize; private int mFontSize;
@AsciiBellBehaviour @AsciiBellBehaviour
int mBellBehaviour = BELL_VIBRATE; int mBellBehaviour = BELL_VIBRATE;
boolean mBackIsEscape = true; boolean mBackIsEscape;
boolean mShowExtraKeys;
TermuxPreferences(Context context) { TermuxPreferences(Context context) {
reloadFromProperties(context); reloadFromProperties(context);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, context.getResources().getDisplayMetrics()); float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, context.getResources().getDisplayMetrics());
// This is a bit arbitrary and sub-optimal. We want to give a sensible default for minimum font size // This is a bit arbitrary and sub-optimal. We want to give a sensible default for minimum font size
// to prevent invisible text due to zoom be mistake: // to prevent invisible text due to zoom be mistake:
MIN_FONTSIZE = (int) (4f * dipInPixels); MIN_FONTSIZE = (int) (4f * dipInPixels);
mFullScreen = prefs.getBoolean(FULLSCREEN_KEY, false); mFullScreen = prefs.getBoolean(FULLSCREEN_KEY, false);
mShowExtraKeys = prefs.getBoolean(SHOW_EXTRA_KEYS_KEY, false);
// http://www.google.com/design/spec/style/typography.html#typography-line-height // http://www.google.com/design/spec/style/typography.html#typography-line-height
int defaultFontSize = Math.round(12 * dipInPixels); int defaultFontSize = Math.round(12 * dipInPixels);
// Make it divisible by 2 since that is the minimal adjustment step: // Make it divisible by 2 since that is the minimal adjustment step:
if (defaultFontSize % 2 == 1) defaultFontSize--; if (defaultFontSize % 2 == 1) defaultFontSize--;
try { try {
mFontSize = Integer.parseInt(prefs.getString(FONTSIZE_KEY, Integer.toString(defaultFontSize))); mFontSize = Integer.parseInt(prefs.getString(FONTSIZE_KEY, Integer.toString(defaultFontSize)));
} catch (NumberFormatException | ClassCastException e) { } catch (NumberFormatException | ClassCastException e) {
mFontSize = defaultFontSize; mFontSize = defaultFontSize;
} }
mFontSize = Math.max(MIN_FONTSIZE, Math.min(mFontSize, MAX_FONTSIZE)); mFontSize = Math.max(MIN_FONTSIZE, Math.min(mFontSize, MAX_FONTSIZE));
} }
boolean isFullScreen() { boolean isFullScreen() {
return mFullScreen; return mFullScreen;
} }
void setFullScreen(Context context, boolean newValue) { void setFullScreen(Context context, boolean newValue) {
mFullScreen = newValue; mFullScreen = newValue;
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(FULLSCREEN_KEY, newValue).apply();
prefs.edit().putBoolean(FULLSCREEN_KEY, newValue).apply(); }
}
int getFontSize() { boolean isShowExtraKeys() {
return mFontSize; return mShowExtraKeys;
} }
void changeFontSize(Context context, boolean increase) { boolean toggleShowExtraKeys(Context context) {
mFontSize += (increase ? 1 : -1) * 2; mShowExtraKeys = !mShowExtraKeys;
mFontSize = Math.max(MIN_FONTSIZE, Math.min(mFontSize, MAX_FONTSIZE)); PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SHOW_EXTRA_KEYS_KEY, mShowExtraKeys).apply();
return mShowExtraKeys;
}
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); int getFontSize() {
prefs.edit().putString(FONTSIZE_KEY, Integer.toString(mFontSize)).apply(); return mFontSize;
} }
static void storeCurrentSession(Context context, TerminalSession session) { void changeFontSize(Context context, boolean increase) {
PreferenceManager.getDefaultSharedPreferences(context).edit().putString(TermuxPreferences.CURRENT_SESSION_KEY, session.mHandle).commit(); mFontSize += (increase ? 1 : -1) * 2;
} mFontSize = Math.max(MIN_FONTSIZE, Math.min(mFontSize, MAX_FONTSIZE));
static TerminalSession getCurrentSession(TermuxActivity context) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String sessionHandle = PreferenceManager.getDefaultSharedPreferences(context).getString(TermuxPreferences.CURRENT_SESSION_KEY, ""); prefs.edit().putString(FONTSIZE_KEY, Integer.toString(mFontSize)).apply();
for (int i = 0, len = context.mTermService.getSessions().size(); i < len; i++) { }
TerminalSession session = context.mTermService.getSessions().get(i);
if (session.mHandle.equals(sessionHandle)) return session;
}
return null;
}
public static boolean isShowWelcomeDialog(Context context) { static void storeCurrentSession(Context context, TerminalSession session) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SHOW_WELCOME_DIALOG_KEY, true); PreferenceManager.getDefaultSharedPreferences(context).edit().putString(TermuxPreferences.CURRENT_SESSION_KEY, session.mHandle).apply();
} }
public static void disableWelcomeDialog(Context context) { static TerminalSession getCurrentSession(TermuxActivity context) {
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SHOW_WELCOME_DIALOG_KEY, false).apply(); String sessionHandle = PreferenceManager.getDefaultSharedPreferences(context).getString(TermuxPreferences.CURRENT_SESSION_KEY, "");
} for (int i = 0, len = context.mTermService.getSessions().size(); i < len; i++) {
TerminalSession session = context.mTermService.getSessions().get(i);
if (session.mHandle.equals(sessionHandle)) return session;
}
return null;
}
public void reloadFromProperties(Context context) { public void reloadFromProperties(Context context) {
try { try {
File propsFile = new File(TermuxService.HOME_PATH + "/.config/termux/termux.properties"); File propsFile = new File(TermuxService.HOME_PATH + "/.termux/termux.properties");
Properties props = new Properties(); if (!propsFile.exists())
if (propsFile.isFile() && propsFile.canRead()) { propsFile = new File(TermuxService.HOME_PATH + "/.config/termux/termux.properties");
try (FileInputStream in = new FileInputStream(propsFile)) {
props.load(in);
}
}
switch (props.getProperty("bell-character", "vibrate")) { Properties props = new Properties();
case "beep": if (propsFile.isFile() && propsFile.canRead()) {
mBellBehaviour = BELL_BEEP; try (FileInputStream in = new FileInputStream(propsFile)) {
break; props.load(in);
case "ignore": }
mBellBehaviour = BELL_IGNORE; }
break;
default: // "vibrate".
mBellBehaviour = BELL_VIBRATE;
break;
}
mBackIsEscape = "escape".equals(props.getProperty("back-key", "back")); switch (props.getProperty("bell-character", "vibrate")) {
} catch (Exception e) { case "beep":
Toast.makeText(context, "Error loading properties: " + e.getMessage(), Toast.LENGTH_LONG).show(); mBellBehaviour = BELL_BEEP;
Log.e("termux", "Error loading props", e); break;
} case "ignore":
} mBellBehaviour = BELL_IGNORE;
break;
default: // "vibrate".
mBellBehaviour = BELL_VIBRATE;
break;
}
mBackIsEscape = "escape".equals(props.getProperty("back-key", "back"));
shortcuts.clear();
parseAction("shortcut.create-session", SHORTCUT_ACTION_CREATE_SESSION, props);
parseAction("shortcut.next-session", SHORTCUT_ACTION_NEXT_SESSION, props);
parseAction("shortcut.previous-session", SHORTCUT_ACTION_PREVIOUS_SESSION, props);
parseAction("shortcut.rename-session", SHORTCUT_ACTION_RENAME_SESSION, props);
} catch (Exception e) {
Toast.makeText(context, "Error loading properties: " + e.getMessage(), Toast.LENGTH_LONG).show();
Log.e("termux", "Error loading props", e);
}
}
public static final int SHORTCUT_ACTION_CREATE_SESSION = 1;
public static final int SHORTCUT_ACTION_NEXT_SESSION = 2;
public static final int SHORTCUT_ACTION_PREVIOUS_SESSION = 3;
public static final int SHORTCUT_ACTION_RENAME_SESSION = 4;
public final static class KeyboardShortcut {
public KeyboardShortcut(int codePoint, int shortcutAction) {
this.codePoint = codePoint;
this.shortcutAction = shortcutAction;
}
final int codePoint;
final int shortcutAction;
}
final List<KeyboardShortcut> shortcuts = new ArrayList<>();
private void parseAction(String name, int shortcutAction, Properties props) {
String value = props.getProperty(name);
if (value == null) return;
String[] parts = value.toLowerCase().trim().split("\\+");
String input = parts.length == 2 ? parts[1].trim() : null;
if (!(parts.length == 2 && parts[0].trim().equals("ctrl")) || input.isEmpty() || input.length() > 2) {
Log.e("termux", "Keyboard shortcut '" + name + "' is not Ctrl+<something>");
return;
}
char c = input.charAt(0);
int codePoint = c;
if (Character.isLowSurrogate(c)) {
if (input.length() != 2 || Character.isHighSurrogate(input.charAt(1))) {
Log.e("termux", "Keyboard shortcut '" + name + "' is not Ctrl+<something>");
return;
} else {
codePoint = Character.toCodePoint(input.charAt(1), c);
}
}
shortcuts.add(new KeyboardShortcut(codePoint, shortcutAction));
}
} }

View File

@@ -1,15 +1,5 @@
package com.termux.app; package com.termux.app;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import com.termux.R;
import com.termux.terminal.EmulatorDebug;
import com.termux.terminal.TerminalSession;
import com.termux.terminal.TerminalSession.SessionChangedCallback;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationManager; import android.app.NotificationManager;
@@ -21,335 +11,334 @@ import android.content.res.Resources;
import android.net.Uri; import android.net.Uri;
import android.net.wifi.WifiManager; import android.net.wifi.WifiManager;
import android.os.Binder; import android.os.Binder;
import android.os.Handler;
import android.os.IBinder; import android.os.IBinder;
import android.os.PowerManager; import android.os.PowerManager;
import android.support.v4.content.WakefulBroadcastReceiver;
import android.util.Log; import android.util.Log;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import com.termux.R;
import com.termux.terminal.EmulatorDebug;
import com.termux.terminal.TerminalSession;
import com.termux.terminal.TerminalSession.SessionChangedCallback;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/** /**
* A service holding a list of terminal sessions, {@link #mTerminalSessions}, showing a foreground notification while * A service holding a list of terminal sessions, {@link #mTerminalSessions}, showing a foreground notification while
* running so that it is not terminated. The user interacts with the session through {@link TermuxActivity}, but this * running so that it is not terminated. The user interacts with the session through {@link TermuxActivity}, but this
* service may outlive the activity when the user or the system disposes of the activity. In that case the user may * service may outlive the activity when the user or the system disposes of the activity. In that case the user may
* restart {@link TermuxActivity} later to yet again access the sessions. * restart {@link TermuxActivity} later to yet again access the sessions.
* * <p/>
* In order to keep both terminal sessions and spawned processes (who may outlive the terminal sessions) alive as long * In order to keep both terminal sessions and spawned processes (who may outlive the terminal sessions) alive as long
* as wanted by the user this service is a foreground service, {@link Service#startForeground(int, Notification)}. * as wanted by the user this service is a foreground service, {@link Service#startForeground(int, Notification)}.
* * <p/>
* Optionally may hold a wake and a wifi lock, in which case that is shown in the notification - see * Optionally may hold a wake and a wifi lock, in which case that is shown in the notification - see
* {@link #buildNotification()}. * {@link #buildNotification()}.
*/ */
public final class TermuxService extends Service implements SessionChangedCallback { public final class TermuxService extends Service implements SessionChangedCallback {
/** Note that this is a symlink on the Android M preview. */ /** Note that this is a symlink on the Android M preview. */
@SuppressLint("SdCardPath") @SuppressLint("SdCardPath")
public static final String FILES_PATH = "/data/data/com.termux/files"; public static final String FILES_PATH = "/data/data/com.termux/files";
public static final String PREFIX_PATH = FILES_PATH + "/usr"; public static final String PREFIX_PATH = FILES_PATH + "/usr";
public static final String HOME_PATH = FILES_PATH + "/home"; public static final String HOME_PATH = FILES_PATH + "/home";
private static final int NOTIFICATION_ID = 1337; private static final int NOTIFICATION_ID = 1337;
/** Intent action to stop the service. */ private static final String ACTION_STOP_SERVICE = "com.termux.service_stop";
private static final String ACTION_STOP_SERVICE = "com.termux.service_stop"; private static final String ACTION_LOCK_WAKE = "com.termux.service_wake_lock";
/** Intent action to toggle the wake lock, {@link #mWakeLock}, which this service may hold. */ private static final String ACTION_UNLOCK_WAKE = "com.termux.service_wake_unlock";
private static final String ACTION_LOCK_WAKE = "com.termux.service_toggle_wake_lock"; /** Intent action to launch a new terminal session. Executed from TermuxWidgetProvider. */
/** Intent action to toggle the wifi lock, {@link #mWifiLock}, which this service may hold. */ public static final String ACTION_EXECUTE = "com.termux.service_execute";
private static final String ACTION_LOCK_WIFI = "com.termux.service_toggle_wifi_lock";
/** Intent action to launch a new terminal session. Executed from TermuxWidgetProvider. */
public static final String ACTION_EXECUTE = "com.termux.service_execute";
public static final String EXTRA_ARGUMENTS = "com.termux.execute.arguments"; public static final String EXTRA_ARGUMENTS = "com.termux.execute.arguments";
public static final String EXTRA_CURRENT_WORKING_DIRECTORY = "com.termux.execute.cwd"; public static final String EXTRA_CURRENT_WORKING_DIRECTORY = "com.termux.execute.cwd";
private static final String EXTRA_EXECUTE_IN_BACKGROUND = "com.termux.execute.background";
/** This service is only bound from inside the same process and never uses IPC. */ /** This service is only bound from inside the same process and never uses IPC. */
class LocalBinder extends Binder { class LocalBinder extends Binder {
public final TermuxService service = TermuxService.this; public final TermuxService service = TermuxService.this;
} }
private final IBinder mBinder = new LocalBinder(); private final IBinder mBinder = new LocalBinder();
/** private final Handler mHandler = new Handler();
* The terminal sessions which this service manages.
*
* Note that this list is observed by {@link TermuxActivity#mListViewAdapter}, so any changes must be made on the UI
* thread and followed by a call to {@link ArrayAdapter#notifyDataSetChanged()} }.
*/
final List<TerminalSession> mTerminalSessions = new ArrayList<>();
/** Note that the service may often outlive the activity, so need to clear this reference. */ /**
SessionChangedCallback mSessionChangeCallback; * The terminal sessions which this service manages.
* <p/>
* Note that this list is observed by {@link TermuxActivity#mListViewAdapter}, so any changes must be made on the UI
* thread and followed by a call to {@link ArrayAdapter#notifyDataSetChanged()} }.
*/
final List<TerminalSession> mTerminalSessions = new ArrayList<>();
private PowerManager.WakeLock mWakeLock; final List<BackgroundJob> mBackgroundTasks = new ArrayList<>();
private WifiManager.WifiLock mWifiLock;
/** If the user has executed the {@link #ACTION_STOP_SERVICE} intent. */ /** Note that the service may often outlive the activity, so need to clear this reference. */
boolean mWantsToStop = false; SessionChangedCallback mSessionChangeCallback;
@SuppressLint("Wakelock") /** The wake lock and wifi lock are always acquired and released together. */
@Override private PowerManager.WakeLock mWakeLock;
public int onStartCommand(Intent intent, int flags, int startId) { private WifiManager.WifiLock mWifiLock;
String action = intent.getAction();
if (ACTION_STOP_SERVICE.equals(action)) {
mWantsToStop = true;
for (int i = 0; i < mTerminalSessions.size(); i++)
mTerminalSessions.get(i).finishIfRunning();
stopSelf();
} else if (ACTION_LOCK_WAKE.equals(action)) {
if (mWakeLock == null) {
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, EmulatorDebug.LOG_TAG);
mWakeLock.acquire();
} else {
mWakeLock.release();
mWakeLock = null;
}
updateNotification();
} else if (ACTION_LOCK_WIFI.equals(action)) {
if (mWifiLock == null) {
WifiManager wm = (WifiManager) getSystemService(Context.WIFI_SERVICE);
mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, EmulatorDebug.LOG_TAG);
mWifiLock.acquire();
} else {
mWifiLock.release();
mWifiLock = null;
}
updateNotification();
} else if (ACTION_EXECUTE.equals(action)) {
Uri executableUri = intent.getData();
String executablePath = (executableUri == null ? null : executableUri.getPath());
String[] arguments = (executableUri == null ? null : intent.getStringArrayExtra(EXTRA_ARGUMENTS));
String cwd = intent.getStringExtra(EXTRA_CURRENT_WORKING_DIRECTORY);
TerminalSession newSession = createTermSession(executablePath, arguments, cwd, false);
// Transform executable path to session name, e.g. "/bin/do-something.sh" => "do something.sh". /** If the user has executed the {@link #ACTION_STOP_SERVICE} intent. */
if (executablePath != null) { boolean mWantsToStop = false;
int lastSlash = executablePath.lastIndexOf('/');
String name = (lastSlash == -1) ? executablePath : executablePath.substring(lastSlash + 1);
name = name.replace('-', ' ');
newSession.mSessionName = name;
}
// Make the newly created session the current one to be displayed: @SuppressLint("Wakelock")
TermuxPreferences.storeCurrentSession(this, newSession); @Override
public int onStartCommand(Intent intent, int flags, int startId) {
String action = intent.getAction();
if (ACTION_STOP_SERVICE.equals(action)) {
mWantsToStop = true;
for (int i = 0; i < mTerminalSessions.size(); i++)
mTerminalSessions.get(i).finishIfRunning();
stopSelf();
} else if (ACTION_LOCK_WAKE.equals(action)) {
if (mWakeLock == null) {
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, EmulatorDebug.LOG_TAG);
mWakeLock.acquire();
// Launch the main Termux app, which will now show to current session: // http://tools.android.com/tech-docs/lint-in-studio-2-3#TOC-WifiManager-Leak
startActivity(new Intent(this, TermuxActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); WifiManager wm = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
} else if (action != null) { mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, EmulatorDebug.LOG_TAG);
Log.e(EmulatorDebug.LOG_TAG, "Unknown TermuxService action: '" + action + "'"); mWifiLock.acquire();
}
// If this service really do get killed, there is no point restarting it automatically - let the user do on next updateNotification();
// start of {@link Term): }
return Service.START_NOT_STICKY; } else if (ACTION_UNLOCK_WAKE.equals(action)) {
} if (mWakeLock != null) {
mWakeLock.release();
mWakeLock = null;
@Override mWifiLock.release();
public IBinder onBind(Intent intent) { mWifiLock = null;
return mBinder;
}
@Override updateNotification();
public void onCreate() { }
startForeground(NOTIFICATION_ID, buildNotification()); } else if (ACTION_EXECUTE.equals(action)) {
} Uri executableUri = intent.getData();
String executablePath = (executableUri == null ? null : executableUri.getPath());
/** Update the shown foreground service notification after making any changes that affect it. */ String[] arguments = (executableUri == null ? null : intent.getStringArrayExtra(EXTRA_ARGUMENTS));
private void updateNotification() { String cwd = intent.getStringExtra(EXTRA_CURRENT_WORKING_DIRECTORY);
if (mWakeLock == null && mWifiLock == null && getSessions().isEmpty()) {
// Exit if we are updating after the user disabled all locks with no sessions.
stopSelf();
} else {
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, buildNotification());
}
}
private Notification buildNotification() { if (intent.getBooleanExtra(EXTRA_EXECUTE_IN_BACKGROUND, false)) {
Intent notifyIntent = new Intent(this, TermuxActivity.class); BackgroundJob task = new BackgroundJob(cwd, executablePath, arguments, this);
// PendingIntent#getActivity(): "Note that the activity will be started outside of the context of an existing mBackgroundTasks.add(task);
// activity, so you must use the Intent.FLAG_ACTIVITY_NEW_TASK launch flag in the Intent": updateNotification();
notifyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } else {
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notifyIntent, 0); TerminalSession newSession = createTermSession(executablePath, arguments, cwd, false);
int sessionCount = mTerminalSessions.size(); // Transform executable path to session name, e.g. "/bin/do-something.sh" => "do something.sh".
String contentText = sessionCount + " terminal session" + (sessionCount == 1 ? "" : "s"); if (executablePath != null) {
int lastSlash = executablePath.lastIndexOf('/');
String name = (lastSlash == -1) ? executablePath : executablePath.substring(lastSlash + 1);
name = name.replace('-', ' ');
newSession.mSessionName = name;
}
boolean wakeLockHeld = mWakeLock != null; // Make the newly created session the current one to be displayed:
boolean wifiLockHeld = mWifiLock != null; TermuxPreferences.storeCurrentSession(this, newSession);
if (wakeLockHeld && wifiLockHeld) {
contentText += " (wake&wifi lock held)";
} else if (wakeLockHeld) {
contentText += " (wake lock held)";
} else if (wifiLockHeld) {
contentText += " (wifi lock held)";
}
Notification.Builder builder = new Notification.Builder(this); // Launch the main Termux app, which will now show the current session:
builder.setContentTitle(getText(R.string.application_name)); startActivity(new Intent(this, TermuxActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
builder.setContentText(contentText); }
builder.setSmallIcon(R.drawable.ic_service_notification); } else if (action != null) {
builder.setContentIntent(pendingIntent); Log.e(EmulatorDebug.LOG_TAG, "Unknown TermuxService action: '" + action + "'");
builder.setOngoing(true); }
// If holding a wake or wifi lock consider the notification of high priority since it's using power, if ((flags & START_FLAG_REDELIVERY) == 0) {
// otherwise use a minimal priority since this is just a background service notification: // Service is started by WBR, not restarted by system, so release the WakeLock from WBR.
builder.setPriority((wakeLockHeld || wifiLockHeld) ? Notification.PRIORITY_HIGH : Notification.PRIORITY_MIN); WakefulBroadcastReceiver.completeWakefulIntent(intent);
}
// No need to show a timestamp: // If this service really do get killed, there is no point restarting it automatically - let the user do on next
builder.setShowWhen(false); // start of {@link Term):
return Service.START_NOT_STICKY;
}
// Background color for small notification icon: @Override
builder.setColor(0xFF000000); public IBinder onBind(Intent intent) {
return mBinder;
}
Resources res = getResources(); @Override
Intent exitIntent = new Intent(this, TermuxService.class).setAction(ACTION_STOP_SERVICE); public void onCreate() {
builder.addAction(android.R.drawable.ic_delete, res.getString(R.string.notification_action_exit), PendingIntent.getService(this, 0, exitIntent, 0)); startForeground(NOTIFICATION_ID, buildNotification());
}
Intent toggleWakeLockIntent = new Intent(this, TermuxService.class).setAction(ACTION_LOCK_WAKE); /** Update the shown foreground service notification after making any changes that affect it. */
builder.addAction(android.R.drawable.ic_lock_lock, res.getString(R.string.notification_action_wakelock), void updateNotification() {
PendingIntent.getService(this, 0, toggleWakeLockIntent, 0)); if (mWakeLock == null && mTerminalSessions.isEmpty() && mBackgroundTasks.isEmpty()) {
// Exit if we are updating after the user disabled all locks with no sessions or tasks running.
stopSelf();
} else {
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, buildNotification());
}
}
Intent toggleWifiLockIntent = new Intent(this, TermuxService.class).setAction(ACTION_LOCK_WIFI); private Notification buildNotification() {
builder.addAction(android.R.drawable.ic_lock_lock, res.getString(R.string.notification_action_wifilock), Intent notifyIntent = new Intent(this, TermuxActivity.class);
PendingIntent.getService(this, 0, toggleWifiLockIntent, 0)); // PendingIntent#getActivity(): "Note that the activity will be started outside of the context of an existing
// activity, so you must use the Intent.FLAG_ACTIVITY_NEW_TASK launch flag in the Intent":
notifyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notifyIntent, 0);
return builder.build(); int sessionCount = mTerminalSessions.size();
} int taskCount = mBackgroundTasks.size();
String contentText = sessionCount + " session" + (sessionCount == 1 ? "" : "s");
if (taskCount > 0) {
contentText += ", " + taskCount + " task" + (taskCount == 1 ? "" : "s");
}
@Override final boolean wakeLockHeld = mWakeLock != null;
public void onDestroy() { if (wakeLockHeld) contentText += " (wake lock held)";
if (mWakeLock != null) mWakeLock.release();
if (mWifiLock != null) mWifiLock.release();
stopForeground(true); Notification.Builder builder = new Notification.Builder(this);
builder.setContentTitle(getText(R.string.application_name));
builder.setContentText(contentText);
builder.setSmallIcon(R.drawable.ic_service_notification);
builder.setContentIntent(pendingIntent);
builder.setOngoing(true);
for (int i = 0; i < mTerminalSessions.size(); i++) // If holding a wake or wifi lock consider the notification of high priority since it's using power,
mTerminalSessions.get(i).finishIfRunning(); // otherwise use a minimal priority since this is just a background service notification:
mTerminalSessions.clear(); builder.setPriority((wakeLockHeld) ? Notification.PRIORITY_HIGH : Notification.PRIORITY_MIN);
}
public List<TerminalSession> getSessions() { // No need to show a timestamp:
return mTerminalSessions; builder.setShowWhen(false);
}
TerminalSession createTermSession(String executablePath, String[] arguments, String cwd, boolean failSafe) { // Background color for small notification icon:
new File(HOME_PATH).mkdirs(); builder.setColor(0xFF000000);
if (cwd == null) cwd = HOME_PATH; Resources res = getResources();
Intent exitIntent = new Intent(this, TermuxService.class).setAction(ACTION_STOP_SERVICE);
builder.addAction(android.R.drawable.ic_delete, res.getString(R.string.notification_action_exit), PendingIntent.getService(this, 0, exitIntent, 0));
final String termEnv = "TERM=xterm-256color"; String newWakeAction = wakeLockHeld ? ACTION_UNLOCK_WAKE : ACTION_LOCK_WAKE;
final String homeEnv = "HOME=" + HOME_PATH; Intent toggleWakeLockIntent = new Intent(this, TermuxService.class).setAction(newWakeAction);
final String prefixEnv = "PREFIX=" + PREFIX_PATH; String actionTitle = res.getString(wakeLockHeld ?
final String androidRootEnv = "ANDROID_ROOT=" + System.getenv("ANDROID_ROOT"); R.string.notification_action_wake_unlock :
final String androidDataEnv = "ANDROID_DATA=" + System.getenv("ANDROID_DATA"); R.string.notification_action_wake_lock);
// EXTERNAL_STORAGE is needed for /system/bin/am to work on at least int actionIcon = wakeLockHeld ? android.R.drawable.ic_lock_idle_lock : android.R.drawable.ic_lock_lock;
// Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3. builder.addAction(actionIcon, actionTitle, PendingIntent.getService(this, 0, toggleWakeLockIntent, 0));
final String externalStorageEnv = "EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE");
String[] env;
if (failSafe) {
// Keep the default path so that system binaries can be used in the failsafe session.
final String pathEnv = "PATH=" + System.getenv("PATH");
env = new String[] { termEnv, homeEnv, prefixEnv, androidRootEnv, androidDataEnv, pathEnv, externalStorageEnv };
} else {
final String ps1Env = "PS1=$ ";
final String ldEnv = "LD_LIBRARY_PATH=" + PREFIX_PATH + "/lib";
final String langEnv = "LANG=en_US.UTF-8";
final String pathEnv = "PATH=" + PREFIX_PATH + "/bin:" + PREFIX_PATH + "/bin/applets";
final String pwdEnv = "PWD=" + cwd;
env = new String[] { termEnv, homeEnv, prefixEnv, ps1Env, ldEnv, langEnv, pathEnv, pwdEnv, androidRootEnv, androidDataEnv, externalStorageEnv }; return builder.build();
} }
String shellName; @Override
if (executablePath == null) { public void onDestroy() {
File shell = new File(HOME_PATH, ".termux/shell"); if (mWakeLock != null) mWakeLock.release();
if (shell.exists()) { if (mWifiLock != null) mWifiLock.release();
try {
File canonicalFile = shell.getCanonicalFile();
if (canonicalFile.isFile() && canonicalFile.canExecute()) {
executablePath = canonicalFile.getName().equals("busybox") ? (PREFIX_PATH + "/bin/ash") : canonicalFile.getAbsolutePath();
} else {
Log.w(EmulatorDebug.LOG_TAG, "$HOME/.termux/shell points to non-executable shell: " + canonicalFile.getAbsolutePath());
}
} catch (IOException e) {
Log.e(EmulatorDebug.LOG_TAG, "Error checking $HOME/.termux/shell", e);
}
}
if (executablePath == null) { stopForeground(true);
// Try bash, zsh and ash in that order:
for (String shellBinary : new String[] { "bash", "zsh", "ash" }) {
File shellFile = new File(PREFIX_PATH + "/bin/" + shellBinary);
if (shellFile.canExecute()) {
executablePath = shellFile.getAbsolutePath();
break;
}
}
}
if (executablePath == null) { for (int i = 0; i < mTerminalSessions.size(); i++)
// Fall back to system shell as last resort: mTerminalSessions.get(i).finishIfRunning();
executablePath = "/system/bin/sh"; mTerminalSessions.clear();
} }
String[] parts = executablePath.split("/"); public List<TerminalSession> getSessions() {
shellName = "-" + parts[parts.length - 1]; return mTerminalSessions;
} else { }
int lastSlashIndex = executablePath.lastIndexOf('/');
shellName = lastSlashIndex == -1 ? executablePath : executablePath.substring(lastSlashIndex + 1);
}
String[] args; TerminalSession createTermSession(String executablePath, String[] arguments, String cwd, boolean failSafe) {
if (arguments == null) { new File(HOME_PATH).mkdirs();
args = new String[] { shellName };
} else {
args = new String[arguments.length + 1];
args[0] = shellName;
System.arraycopy(arguments, 0, args, 1, arguments.length); if (cwd == null) cwd = HOME_PATH;
}
TerminalSession session = new TerminalSession(executablePath, cwd, args, env, this); String[] env = BackgroundJob.buildEnvironment(failSafe, cwd);
mTerminalSessions.add(session); boolean isLoginShell = false;
updateNotification();
return session;
}
public int removeTermSession(TerminalSession sessionToRemove) { if (executablePath == null) {
int indexOfRemoved = mTerminalSessions.indexOf(sessionToRemove); for (String shellBinary : new String[]{"login", "bash", "zsh"}) {
mTerminalSessions.remove(indexOfRemoved); File shellFile = new File(PREFIX_PATH + "/bin/" + shellBinary);
if (mTerminalSessions.isEmpty() && mWakeLock == null) { if (shellFile.canExecute()) {
// Finish if there are no sessions left and the wake lock is not held, otherwise keep the service alive if executablePath = shellFile.getAbsolutePath();
// holding wake lock since there may be daemon processes (e.g. sshd) running. break;
stopSelf(); }
} else { }
updateNotification();
}
return indexOfRemoved;
}
@Override if (executablePath == null) {
public void onTitleChanged(TerminalSession changedSession) { // Fall back to system shell as last resort:
if (mSessionChangeCallback != null) mSessionChangeCallback.onTitleChanged(changedSession); executablePath = "/system/bin/sh";
} }
isLoginShell = true;
}
@Override String[] processArgs = BackgroundJob.setupProcessArgs(executablePath, arguments);
public void onSessionFinished(final TerminalSession finishedSession) { executablePath = processArgs[0];
if (mSessionChangeCallback != null) mSessionChangeCallback.onSessionFinished(finishedSession); int lastSlashIndex = executablePath.lastIndexOf('/');
} String processName = (isLoginShell ? "-" : "") +
(lastSlashIndex == -1 ? executablePath : executablePath.substring(lastSlashIndex + 1));
@Override String[] args = new String[processArgs.length];
public void onTextChanged(TerminalSession changedSession) { args[0] = processName;
if (mSessionChangeCallback != null) mSessionChangeCallback.onTextChanged(changedSession); if (processArgs.length > 1) System.arraycopy(processArgs, 1, args, 1, processArgs.length - 1);
}
@Override TerminalSession session = new TerminalSession(executablePath, cwd, args, env, this);
public void onClipboardText(TerminalSession session, String text) { mTerminalSessions.add(session);
if (mSessionChangeCallback != null) mSessionChangeCallback.onClipboardText(session, text); updateNotification();
} return session;
}
@Override public int removeTermSession(TerminalSession sessionToRemove) {
public void onBell(TerminalSession session) { int indexOfRemoved = mTerminalSessions.indexOf(sessionToRemove);
if (mSessionChangeCallback != null) mSessionChangeCallback.onBell(session); mTerminalSessions.remove(indexOfRemoved);
} if (mTerminalSessions.isEmpty() && mWakeLock == null) {
// Finish if there are no sessions left and the wake lock is not held, otherwise keep the service alive if
// holding wake lock since there may be daemon processes (e.g. sshd) running.
stopSelf();
} else {
updateNotification();
}
return indexOfRemoved;
}
@Override
public void onTitleChanged(TerminalSession changedSession) {
if (mSessionChangeCallback != null) mSessionChangeCallback.onTitleChanged(changedSession);
}
@Override
public void onSessionFinished(final TerminalSession finishedSession) {
if (mSessionChangeCallback != null)
mSessionChangeCallback.onSessionFinished(finishedSession);
}
@Override
public void onTextChanged(TerminalSession changedSession) {
if (mSessionChangeCallback != null) mSessionChangeCallback.onTextChanged(changedSession);
}
@Override
public void onClipboardText(TerminalSession session, String text) {
if (mSessionChangeCallback != null) mSessionChangeCallback.onClipboardText(session, text);
}
@Override
public void onBell(TerminalSession session) {
if (mSessionChangeCallback != null) mSessionChangeCallback.onBell(session);
}
@Override
public void onColorsChanged(TerminalSession session) {
if (mSessionChangeCallback != null) mSessionChangeCallback.onColorsChanged(session);
}
public void onBackgroundJobExited(final BackgroundJob task) {
mHandler.post(new Runnable() {
@Override
public void run() {
mBackgroundTasks.remove(task);
updateNotification();
}
});
}
} }

View File

@@ -0,0 +1,281 @@
package com.termux.app;
import android.content.Context;
import android.media.AudioManager;
import android.support.v4.widget.DrawerLayout;
import android.view.Gravity;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.inputmethod.InputMethodManager;
import com.termux.terminal.KeyHandler;
import com.termux.terminal.TerminalEmulator;
import com.termux.terminal.TerminalSession;
import com.termux.view.TerminalViewClient;
import java.util.List;
public final class TermuxViewClient implements TerminalViewClient {
final TermuxActivity mActivity;
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
public TermuxViewClient(TermuxActivity activity) {
this.mActivity = activity;
}
@Override
public float onScale(float scale) {
if (scale < 0.9f || scale > 1.1f) {
boolean increase = scale > 1.f;
mActivity.changeFontSize(increase);
return 1.0f;
}
return scale;
}
@Override
public void onSingleTapUp(MotionEvent e) {
InputMethodManager mgr = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
mgr.showSoftInput(mActivity.mTerminalView, InputMethodManager.SHOW_IMPLICIT);
}
@Override
public boolean shouldBackButtonBeMappedToEscape() {
return mActivity.mSettings.mBackIsEscape;
}
@Override
public void copyModeChanged(boolean copyMode) {
// Disable drawer while copying.
mActivity.getDrawer().setDrawerLockMode(copyMode ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED : DrawerLayout.LOCK_MODE_UNLOCKED);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession currentSession) {
if (handleVirtualKeys(keyCode, e, true)) return true;
if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) {
mActivity.removeFinishedSession(currentSession);
return true;
} else if (e.isCtrlPressed() && e.isAltPressed()) {
// Get the unmodified code point:
int unicodeChar = e.getUnicodeChar(0);
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || unicodeChar == 'n'/* next */) {
mActivity.switchToSession(true);
} else if (keyCode == KeyEvent.KEYCODE_DPAD_UP || unicodeChar == 'p' /* previous */) {
mActivity.switchToSession(false);
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
mActivity.getDrawer().openDrawer(Gravity.LEFT);
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
mActivity.getDrawer().closeDrawers();
} else if (unicodeChar == 'f'/* full screen */) {
mActivity.toggleImmersive();
} else if (unicodeChar == 'k'/* keyboard */) {
InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
} else if (unicodeChar == 'm'/* menu */) {
mActivity.mTerminalView.showContextMenu();
} else if (unicodeChar == 'r'/* rename */) {
mActivity.renameSession(currentSession);
} else if (unicodeChar == 'c'/* create */) {
mActivity.addNewSession(false, null);
} else if (unicodeChar == 'u' /* urls */) {
mActivity.showUrlSelection();
} else if (unicodeChar == 'v') {
mActivity.doPaste();
} else if (unicodeChar == '+' || e.getUnicodeChar(KeyEvent.META_SHIFT_ON) == '+') {
// We also check for the shifted char here since shift may be required to produce '+',
// see https://github.com/termux/termux-api/issues/2
mActivity.changeFontSize(true);
} else if (unicodeChar == '-') {
mActivity.changeFontSize(false);
} else if (unicodeChar >= '1' && unicodeChar <= '9') {
int num = unicodeChar - '1';
TermuxService service = mActivity.mTermService;
if (service.getSessions().size() > num)
mActivity.switchToSession(service.getSessions().get(num));
}
return true;
}
return false;
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent e) {
return handleVirtualKeys(keyCode, e, false);
}
@Override
public boolean readControlKey() {
return (mActivity.mExtraKeysView != null && mActivity.mExtraKeysView.readControlButton()) || mVirtualControlKeyDown;
}
@Override
public boolean readAltKey() {
return (mActivity.mExtraKeysView != null && mActivity.mExtraKeysView.readAltButton());
}
@Override
public boolean onCodePoint(final int codePoint, boolean ctrlDown, TerminalSession session) {
if (mVirtualFnKeyDown) {
int resultingKeyCode = -1;
int resultingCodePoint = -1;
boolean altDown = false;
int lowerCase = Character.toLowerCase(codePoint);
switch (lowerCase) {
// Arrow keys.
case 'w':
resultingKeyCode = KeyEvent.KEYCODE_DPAD_UP;
break;
case 'a':
resultingKeyCode = KeyEvent.KEYCODE_DPAD_LEFT;
break;
case 's':
resultingKeyCode = KeyEvent.KEYCODE_DPAD_DOWN;
break;
case 'd':
resultingKeyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
break;
// Page up and down.
case 'p':
resultingKeyCode = KeyEvent.KEYCODE_PAGE_UP;
break;
case 'n':
resultingKeyCode = KeyEvent.KEYCODE_PAGE_DOWN;
break;
// Some special keys:
case 't':
resultingKeyCode = KeyEvent.KEYCODE_TAB;
break;
case 'i':
resultingKeyCode = KeyEvent.KEYCODE_INSERT;
break;
case 'h':
resultingCodePoint = '~';
break;
// Special characters to input.
case 'u':
resultingCodePoint = '_';
break;
case 'l':
resultingCodePoint = '|';
break;
// Function keys.
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
resultingKeyCode = (codePoint - '1') + KeyEvent.KEYCODE_F1;
break;
case '0':
resultingKeyCode = KeyEvent.KEYCODE_F10;
break;
// Other special keys.
case 'e':
resultingCodePoint = /*Escape*/ 27;
break;
case '.':
resultingCodePoint = /*^.*/ 28;
break;
case 'b': // alt+b, jumping backward in readline.
case 'f': // alf+f, jumping forward in readline.
case 'x': // alt+x, common in emacs.
resultingCodePoint = lowerCase;
altDown = true;
break;
// Volume control.
case 'v':
resultingCodePoint = -1;
AudioManager audio = (AudioManager) mActivity.getSystemService(Context.AUDIO_SERVICE);
audio.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME, AudioManager.USE_DEFAULT_STREAM_TYPE, AudioManager.FLAG_SHOW_UI);
break;
// Writing mode:
case 'q':
mActivity.toggleShowExtraKeys();
break;
}
if (resultingKeyCode != -1) {
TerminalEmulator term = session.getEmulator();
session.write(KeyHandler.getCode(resultingKeyCode, 0, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode()));
} else if (resultingCodePoint != -1) {
session.writeCodePoint(altDown, resultingCodePoint);
}
return true;
} else if (ctrlDown) {
if (codePoint == 106 /* Ctrl+j or \n */ && !session.isRunning()) {
mActivity.removeFinishedSession(session);
return true;
}
List<TermuxPreferences.KeyboardShortcut> shortcuts = mActivity.mSettings.shortcuts;
if (!shortcuts.isEmpty()) {
int codePointLowerCase = Character.toLowerCase(codePoint);
for (int i = shortcuts.size() - 1; i >= 0; i--) {
TermuxPreferences.KeyboardShortcut shortcut = shortcuts.get(i);
if (codePointLowerCase == shortcut.codePoint) {
switch (shortcut.shortcutAction) {
case TermuxPreferences.SHORTCUT_ACTION_CREATE_SESSION:
mActivity.addNewSession(false, null);
return true;
case TermuxPreferences.SHORTCUT_ACTION_PREVIOUS_SESSION:
mActivity.switchToSession(false);
return true;
case TermuxPreferences.SHORTCUT_ACTION_NEXT_SESSION:
mActivity.switchToSession(true);
return true;
case TermuxPreferences.SHORTCUT_ACTION_RENAME_SESSION:
mActivity.renameSession(mActivity.getCurrentTermSession());
return true;
}
}
}
}
}
return false;
}
@Override
public boolean onLongPress(MotionEvent event) {
return false;
}
/** 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) {
// Do not steal dedicated buttons from a full external keyboard.
return false;
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
mVirtualControlKeyDown = down;
return true;
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
mVirtualFnKeyDown = down;
return true;
}
return false;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,88 +0,0 @@
package com.termux.drawer;
/*
* Copyright (C) 2014 The Android Open Source Project
*
* 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
*
* http://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.
*/
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
/**
* Provides functionality for DrawerLayout unique to API 21
*/
@SuppressLint("RtlHardcoded")
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
class DrawerLayoutCompatApi21 {
private static final int[] THEME_ATTRS = { android.R.attr.colorPrimaryDark };
public static void configureApplyInsets(DrawerLayout drawerLayout) {
drawerLayout.setOnApplyWindowInsetsListener(new InsetsListener());
drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
}
public static void dispatchChildInsets(View child, Object insets, int gravity) {
WindowInsets wi = (WindowInsets) insets;
if (gravity == Gravity.LEFT) {
wi = wi.replaceSystemWindowInsets(wi.getSystemWindowInsetLeft(), wi.getSystemWindowInsetTop(), 0, wi.getSystemWindowInsetBottom());
} else if (gravity == Gravity.RIGHT) {
wi = wi.replaceSystemWindowInsets(0, wi.getSystemWindowInsetTop(), wi.getSystemWindowInsetRight(), wi.getSystemWindowInsetBottom());
}
child.dispatchApplyWindowInsets(wi);
}
public static void applyMarginInsets(ViewGroup.MarginLayoutParams lp, Object insets, int gravity) {
WindowInsets wi = (WindowInsets) insets;
if (gravity == Gravity.LEFT) {
wi = wi.replaceSystemWindowInsets(wi.getSystemWindowInsetLeft(), wi.getSystemWindowInsetTop(), 0, wi.getSystemWindowInsetBottom());
} else if (gravity == Gravity.RIGHT) {
wi = wi.replaceSystemWindowInsets(0, wi.getSystemWindowInsetTop(), wi.getSystemWindowInsetRight(), wi.getSystemWindowInsetBottom());
}
lp.leftMargin = wi.getSystemWindowInsetLeft();
lp.topMargin = wi.getSystemWindowInsetTop();
lp.rightMargin = wi.getSystemWindowInsetRight();
lp.bottomMargin = wi.getSystemWindowInsetBottom();
}
public static int getTopInset(Object insets) {
return insets != null ? ((WindowInsets) insets).getSystemWindowInsetTop() : 0;
}
public static Drawable getDefaultStatusBarBackground(Context context) {
final TypedArray a = context.obtainStyledAttributes(THEME_ATTRS);
try {
return a.getDrawable(0);
} finally {
a.recycle();
}
}
static class InsetsListener implements View.OnApplyWindowInsetsListener {
@Override
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
final DrawerLayout drawerLayout = (DrawerLayout) v;
drawerLayout.setChildInsets(insets, insets.getSystemWindowInsetTop() > 0);
return insets.consumeSystemWindowInsets();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +0,0 @@
/**
* Extraction (and some minor cleanup to get rid of warnings) of DrawerLayout from the
* <a href="http://developer.android.com/tools/support-library/index.html">Android Support Library</a>.
*
* Source at:
* https://android.googlesource.com/platform/frameworks/support/+/refs/heads/master/v4/java/android/support/v4/widget/DrawerLayout.java
* https://android.googlesource.com/platform/frameworks/support/+/refs/heads/master/v4/java/android/support/v4/widget/ViewDragHelper.java
*/
package com.termux.drawer;

View File

@@ -0,0 +1,242 @@
package com.termux.filepicker;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.graphics.Point;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
import android.provider.DocumentsProvider;
import android.webkit.MimeTypeMap;
import com.termux.R;
import com.termux.app.TermuxService;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
/**
* A document provider for the Storage Access Framework which exposes the files in the
* $HOME/ folder to other apps.
* <p/>
* Note that this replaces providing an activity matching the ACTION_GET_CONTENT intent:
* <p/>
* "A document provider and ACTION_GET_CONTENT should be considered mutually exclusive. If you
* support both of them simultaneously, your app will appear twice in the system picker UI,
* offering two different ways of accessing your stored data. This would be confusing for users."
* - http://developer.android.com/guide/topics/providers/document-provider.html#43
*/
public class TermuxDocumentsProvider extends DocumentsProvider {
private static final String ALL_MIME_TYPES = "*/*";
private static final File BASE_DIR = new File(TermuxService.HOME_PATH);
// The default columns to return information about a root if no specific
// columns are requested in a query.
private static final String[] DEFAULT_ROOT_PROJECTION = new String[]{
Root.COLUMN_ROOT_ID,
Root.COLUMN_MIME_TYPES,
Root.COLUMN_FLAGS,
Root.COLUMN_ICON,
Root.COLUMN_TITLE,
Root.COLUMN_SUMMARY,
Root.COLUMN_DOCUMENT_ID,
Root.COLUMN_AVAILABLE_BYTES
};
// The default columns to return information about a document if no specific
// columns are requested in a query.
private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[]{
Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_MIME_TYPE,
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_LAST_MODIFIED,
Document.COLUMN_FLAGS,
Document.COLUMN_SIZE
};
@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION);
@SuppressWarnings("ConstantConditions") final String applicationName = getContext().getString(R.string.application_name);
final MatrixCursor.RowBuilder row = result.newRow();
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_TITLE, applicationName);
row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES);
row.add(Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.getFreeSpace());
row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher);
return result;
}
@Override
public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
includeFile(result, documentId, null);
return result;
}
@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
final File parent = getFileForDocId(parentDocumentId);
for (File file : parent.listFiles()) {
if (!file.getName().startsWith(".")) {
includeFile(result, null, file);
}
}
return result;
}
@Override
public ParcelFileDescriptor openDocument(final String documentId, String mode, CancellationSignal signal) throws FileNotFoundException {
final File file = getFileForDocId(documentId);
final int accessMode = ParcelFileDescriptor.parseMode(mode);
return ParcelFileDescriptor.open(file, accessMode);
}
@Override
public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
final File file = getFileForDocId(documentId);
final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
return new AssetFileDescriptor(pfd, 0, file.length());
}
@Override
public boolean onCreate() {
return true;
}
@Override
public void deleteDocument(String documentId) throws FileNotFoundException {
File file = getFileForDocId(documentId);
if (!file.delete()) {
throw new FileNotFoundException("Failed to delete document with id " + documentId);
}
}
@Override
public String getDocumentType(String documentId) throws FileNotFoundException {
File file = getFileForDocId(documentId);
return getMimeType(file);
}
@Override
public Cursor querySearchDocuments(String rootId, String query, String[] projection) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
final File parent = getFileForDocId(rootId);
// This example implementation searches file names for the query and doesn't rank search
// results, so we can stop as soon as we find a sufficient number of matches. Other
// implementations might rank results and use other data about files, rather than the file
// name, to produce a match.
final LinkedList<File> pending = new LinkedList<>();
pending.add(parent);
final int MAX_SEARCH_RESULTS = 50;
while (!pending.isEmpty() && result.getCount() < MAX_SEARCH_RESULTS) {
final File file = pending.removeFirst();
// Avoid folders outside the $HOME folders linked in to symlinks (to avoid e.g. search
// through the whole SD card).
boolean isInsideHome;
try {
isInsideHome = file.getCanonicalPath().startsWith(TermuxService.HOME_PATH);
} catch (IOException e) {
isInsideHome = true;
}
final boolean isHidden = file.getName().startsWith(".");
if (isInsideHome && !isHidden) {
if (file.isDirectory()) {
Collections.addAll(pending, file.listFiles());
} else {
if (file.getName().toLowerCase().contains(query)) {
includeFile(result, null, file);
}
}
}
}
return result;
}
/**
* 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.
* <p/>
* The reverse of @{link #getFileForDocId}.
*/
private static String getDocIdForFile(File file) {
return file.getAbsolutePath();
}
/**
* Get the file given a document id (the reverse of {@link #getDocIdForFile(File)}).
*/
private static File getFileForDocId(String docId) throws FileNotFoundException {
final File f = new File(docId);
if (!f.exists()) throw new FileNotFoundException(f.getAbsolutePath() + " not found");
return f;
}
private static String getMimeType(File file) {
if (file.isDirectory()) {
return Document.MIME_TYPE_DIR;
} else {
final String name = file.getName();
final int lastDot = name.lastIndexOf('.');
if (lastDot >= 0) {
final String extension = name.substring(lastDot + 1);
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mime != null) return mime;
}
return "application/octet-stream";
}
}
/**
* Add a representation of a file to a cursor.
*
* @param result the cursor to modify
* @param docId the document ID representing the desired file (may be null if given file)
* @param file the File object representing the desired file (may be null if given docID)
*/
private void includeFile(MatrixCursor result, String docId, File file)
throws FileNotFoundException {
if (docId == null) {
docId = getDocIdForFile(file);
} else {
file = getFileForDocId(docId);
}
int flags = 0;
if (file.isDirectory()) {
if (file.isDirectory() && file.canWrite()) flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
} else if (file.canWrite()) {
flags |= Document.FLAG_SUPPORTS_WRITE | Document.FLAG_SUPPORTS_DELETE;
}
final String displayName = file.getName();
final String mimeType = getMimeType(file);
if (mimeType.startsWith("image/")) flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
final MatrixCursor.RowBuilder row = result.newRow();
row.add(Document.COLUMN_DOCUMENT_ID, docId);
row.add(Document.COLUMN_DISPLAY_NAME, displayName);
row.add(Document.COLUMN_SIZE, file.length());
row.add(Document.COLUMN_MIME_TYPE, mimeType);
row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
row.add(Document.COLUMN_FLAGS, flags);
row.add(Document.COLUMN_ICON, R.mipmap.ic_launcher);
}
}

View File

@@ -1,106 +0,0 @@
package com.termux.filepicker;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ListActivity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import com.termux.R;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/** Activity allowing picking files from the $HOME folder. */
public class TermuxFilePickerActivity extends ListActivity {
@SuppressLint("SdCardPath")
final String TERMUX_HOME = "/data/data/com.termux/files/home";
private File mCurrentDirectory;
private final List<File> mFiles = new ArrayList<>();
private final List<String> mFileNames = new ArrayList<>();
private ArrayAdapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.file_picker);
mAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, mFileNames);
enterDirectory(new File(TERMUX_HOME));
setListAdapter(mAdapter);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
enterDirectory(mCurrentDirectory.getParentFile());
return true;
} else {
return super.onOptionsItemSelected(item);
}
}
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
super.onListItemClick(l, v, position, id);
File requestFile = mFiles.get(position);
if (requestFile.isDirectory()) {
enterDirectory(requestFile);
} else {
Uri returnUri = Uri.withAppendedPath(Uri.parse("content://com.termux.filepicker.provider/"), requestFile.getAbsolutePath());
Intent returnIntent = new Intent().setData(returnUri);
returnIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
setResult(Activity.RESULT_OK, returnIntent);
finish();
}
}
void enterDirectory(File directory) {
getActionBar().setDisplayHomeAsUpEnabled(!directory.getAbsolutePath().equals(TERMUX_HOME));
String title = directory.getAbsolutePath() + "/";
if (title.startsWith(TERMUX_HOME)) {
title = "~" + title.substring(TERMUX_HOME.length(), title.length());
}
setTitle(title);
mCurrentDirectory = directory;
mFiles.clear();
mFileNames.clear();
mFiles.addAll(Arrays.asList(mCurrentDirectory.listFiles()));
Collections.sort(mFiles, new Comparator<File>() {
@Override
public int compare(File f1, File f2) {
final String n1 = f1.getName();
final String n2 = f2.getName();
// Display dot folders last:
if (n1.startsWith(".") && !n2.startsWith(".")) {
return 1;
} else if (n2.startsWith(".") && !n1.startsWith(".")) {
return -1;
}
return n1.compareToIgnoreCase(n2);
}
});
for (File file : mFiles) {
mFileNames.add(file.getName() + (file.isDirectory() ? "/" : ""));
}
mAdapter.notifyDataSetChanged();
}
}

View File

@@ -1,65 +0,0 @@
package com.termux.filepicker;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.support.annotation.NonNull;
import android.webkit.MimeTypeMap;
import java.io.File;
import java.io.FileNotFoundException;
/** Provider of files content uris picked from {@link com.termux.filepicker.TermuxFilePickerActivity}. */
public class TermuxFilePickerProvider extends ContentProvider {
@Override
public boolean onCreate() {
return true;
}
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
return null;
}
@Override
public String getType(@NonNull Uri uri) {
String contentType = null;
String path = uri.getPath();
int lastDotIndex = path.lastIndexOf('.');
String possibleFileExtension = path.substring(lastDotIndex + 1, path.length());
if (possibleFileExtension.contains("/")) {
// The dot was in the path, so not a file extension.
} else {
MimeTypeMap mimeTypes = MimeTypeMap.getSingleton();
// Lower casing makes it work with e.g. "JPG":
contentType = mimeTypes.getMimeTypeFromExtension(possibleFileExtension.toLowerCase());
}
if (contentType == null) contentType = "application/octet-stream";
return contentType;
}
@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {
return null;
}
@Override
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
return 0;
}
@Override
public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
File file = new File(uri.getPath());
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
}

View File

@@ -35,7 +35,7 @@ public class TermuxFileReceiverActivity extends Activity {
* name input dialog to be implicitly dismissed, and we do not want to finish the activity directly * name input dialog to be implicitly dismissed, and we do not want to finish the activity directly
* when showing the error dialog. * when showing the error dialog.
*/ */
private boolean mFinishOnDismissNameDialog = true; boolean mFinishOnDismissNameDialog = true;
@Override @Override
protected void onResume() { protected void onResume() {
@@ -46,16 +46,6 @@ public class TermuxFileReceiverActivity extends Activity {
final String type = intent.getType(); final String type = intent.getType();
final String scheme = intent.getScheme(); final String scheme = intent.getScheme();
if (intent.getExtras() == null) {
Log.e("termux", "NULL EXTRAS");
} else {
for (String key : intent.getExtras().keySet()) {
Object value = intent.getExtras().get(key);
Log.d("termux", String.format("Extra %s %s (%s)", key,
value.toString(), value.getClass().getName()));
}
}
if (Intent.ACTION_SEND.equals(action) && type != null) { if (Intent.ACTION_SEND.equals(action) && type != null) {
final String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT); final String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
final Uri sharedUri = intent.getParcelableExtra(Intent.EXTRA_STREAM); final Uri sharedUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
@@ -74,9 +64,9 @@ public class TermuxFileReceiverActivity extends Activity {
} else { } else {
showErrorDialogAndQuit("Send action without content - nothing to save."); showErrorDialogAndQuit("Send action without content - nothing to save.");
} }
} else if (scheme.equals("content")) { } else if ("content".equals(scheme)) {
handleContentUri(intent.getData(), intent.getStringExtra(Intent.EXTRA_TITLE)); handleContentUri(intent.getData(), intent.getStringExtra(Intent.EXTRA_TITLE));
} else if (scheme.equals("file")) { } else if ("file".equals(scheme)) {
// When e.g. clicking on a downloaded apk: // When e.g. clicking on a downloaded apk:
String path = intent.getData().getPath(); String path = intent.getData().getPath();
File file = new File(path); File file = new File(path);
@@ -87,7 +77,7 @@ public class TermuxFileReceiverActivity extends Activity {
showErrorDialogAndQuit("Cannot open file: " + e.getMessage() + "."); showErrorDialogAndQuit("Cannot open file: " + e.getMessage() + ".");
} }
} else { } else {
showErrorDialogAndQuit("Unhandled scheme: " + intent.getScheme() + "."); showErrorDialogAndQuit("Unable to receive any file or URL.");
} }
} }
@@ -129,54 +119,55 @@ public class TermuxFileReceiverActivity extends Activity {
} }
void promptNameAndSave(final InputStream in, final String attachmentFileName) { void promptNameAndSave(final InputStream in, final String attachmentFileName) {
DialogUtils.textInput(this, R.string.file_received_title, attachmentFileName DialogUtils.textInput(this, R.string.file_received_title, attachmentFileName, R.string.file_received_edit_button, new DialogUtils.TextSetListener() {
, android.R.string.ok, new DialogUtils.TextSetListener() { @Override
@Override public void onTextSet(String text) {
public void onTextSet(final String text) { File outFile = saveStreamWithName(in, text);
if (saveStreamWithName(in, text) == null) return; if (outFile == null) return;
finish();
}
}, R.string.file_received_open_folder_button, new DialogUtils.TextSetListener() {
@Override
public void onTextSet(String text) {
if (saveStreamWithName(in, text) == null) return;
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE); final File editorProgramFile = new File(EDITOR_PROGRAM);
executeIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, TERMUX_RECEIVEDIR); if (!editorProgramFile.isFile()) {
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class); showErrorDialogAndQuit("The following file does not exist:\n$HOME/bin/termux-file-editor\n\n"
startService(executeIntent); + "Create this file as a script or a symlink - it will be called with the received file as only argument.");
finish(); return;
} }
}, R.string.file_received_edit_button, new DialogUtils.TextSetListener() {
@Override
public void onTextSet(String text) {
File outFile = saveStreamWithName(in, text);
if (outFile == null) return;
final File editorProgramFile = new File(EDITOR_PROGRAM); // Do this for the user if necessary:
if (!editorProgramFile.isFile()) { //noinspection ResultOfMethodCallIgnored
showErrorDialogAndQuit("The following file does not exist:\n$HOME/bin/termux-file-editor\n\n" editorProgramFile.setExecutable(true);
+ "Create this file as a script or a symlink - it will be called with the received file as only argument.");
return; final Uri scriptUri = new Uri.Builder().scheme("file").path(EDITOR_PROGRAM).build();
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE, scriptUri);
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
executeIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()});
startService(executeIntent);
finish();
} }
},
R.string.file_received_open_folder_button, new DialogUtils.TextSetListener() {
@Override
public void onTextSet(String text) {
if (saveStreamWithName(in, text) == null) return;
// Do this for the user if necessary: Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE);
editorProgramFile.setExecutable(true); executeIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, TERMUX_RECEIVEDIR);
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
final Uri scriptUri = new Uri.Builder().scheme("file").path(EDITOR_PROGRAM).build(); startService(executeIntent);
finish();
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE, scriptUri); }
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class); },
executeIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()}); android.R.string.cancel, new DialogUtils.TextSetListener() {
startService(executeIntent); @Override
finish(); public void onTextSet(final String text) {
} finish();
}, new DialogInterface.OnDismissListener() { }
@Override }, new DialogInterface.OnDismissListener() {
public void onDismiss(DialogInterface dialog) { @Override
if (mFinishOnDismissNameDialog) finish(); public void onDismiss(DialogInterface dialog) {
} if (mFinishOnDismissNameDialog) finish();
}); }
});
} }
public File saveStreamWithName(InputStream in, String attachmentFileName) { public File saveStreamWithName(InputStream in, String attachmentFileName) {
@@ -211,6 +202,7 @@ public class TermuxFileReceiverActivity extends Activity {
} }
// Do this for the user if necessary: // Do this for the user if necessary:
//noinspection ResultOfMethodCallIgnored
urlOpenerProgramFile.setExecutable(true); urlOpenerProgramFile.setExecutable(true);
final Uri urlOpenerProgramUri = new Uri.Builder().scheme("file").path(URL_OPENER_PROGRAM).build(); final Uri urlOpenerProgramUri = new Uri.Builder().scheme("file").path(URL_OPENER_PROGRAM).build();

View File

@@ -1,108 +0,0 @@
package com.termux.terminal;
/** A circular byte buffer allowing one producer and one consumer thread. */
final class ByteQueue {
private final byte[] mBuffer;
private int mHead;
private int mStoredBytes;
private boolean mOpen = true;
public ByteQueue(int size) {
mBuffer = new byte[size];
}
public synchronized void close() {
mOpen = false;
notify();
}
public synchronized int read(byte[] buffer, boolean block) {
while (mStoredBytes == 0 && mOpen) {
if (block) {
try {
wait();
} catch (InterruptedException e) {
// Ignore.
}
} else {
return 0;
}
}
if (!mOpen) return -1;
int totalRead = 0;
int bufferLength = mBuffer.length;
boolean wasFull = bufferLength == mStoredBytes;
int length = buffer.length;
int offset = 0;
while (length > 0 && mStoredBytes > 0) {
int oneRun = Math.min(bufferLength - mHead, mStoredBytes);
int bytesToCopy = Math.min(length, oneRun);
System.arraycopy(mBuffer, mHead, buffer, offset, bytesToCopy);
mHead += bytesToCopy;
if (mHead >= bufferLength) mHead = 0;
mStoredBytes -= bytesToCopy;
length -= bytesToCopy;
offset += bytesToCopy;
totalRead += bytesToCopy;
}
if (wasFull) notify();
return totalRead;
}
/**
* Attempt to write the specified portion of the provided buffer to the queue.
*
* Returns whether the output was totally written, false if it was closed before.
*/
public boolean write(byte[] buffer, int offset, int lengthToWrite) {
if (lengthToWrite + offset > buffer.length) {
throw new IllegalArgumentException("length + offset > buffer.length");
} else if (lengthToWrite <= 0) {
throw new IllegalArgumentException("length <= 0");
}
final int bufferLength = mBuffer.length;
synchronized (this) {
while (lengthToWrite > 0) {
while (bufferLength == mStoredBytes && mOpen) {
try {
wait();
} catch (InterruptedException e) {
// Ignore.
}
}
if (!mOpen) return false;
final boolean wasEmpty = mStoredBytes == 0;
int bytesToWriteBeforeWaiting = Math.min(lengthToWrite, bufferLength - mStoredBytes);
lengthToWrite -= bytesToWriteBeforeWaiting;
while (bytesToWriteBeforeWaiting > 0) {
int tail = mHead + mStoredBytes;
int oneRun;
if (tail >= bufferLength) {
// Buffer: [.............]
// ________________H_______T
// =>
// Buffer: [.............]
// ___________T____H
// onRun= _____----_
tail = tail - bufferLength;
oneRun = mHead - tail;
} else {
oneRun = bufferLength - tail;
}
int bytesToCopy = Math.min(oneRun, bytesToWriteBeforeWaiting);
System.arraycopy(buffer, offset, mBuffer, tail, bytesToCopy);
offset += bytesToCopy;
bytesToWriteBeforeWaiting -= bytesToCopy;
mStoredBytes += bytesToCopy;
}
if (wasEmpty) notify();
}
}
return true;
}
}

View File

@@ -1,55 +0,0 @@
package com.termux.terminal;
/**
* Native methods for creating and managing pseudoterminal subprocesses. C code is in jni/termux.c.
*/
final class JNI {
static {
System.loadLibrary("termux");
}
/**
* Create a subprocess. Differs from {@link ProcessBuilder} in that a pseudoterminal is used to communicate with the
* subprocess.
*
* Callers are responsible for calling {@link #close(int)} on the returned file descriptor.
*
* @param cmd
* The command to execute
* @param cwd
* The current working directory for the executed command
* @param args
* An array of arguments to the command
* @param envVars
* An array of strings of the form "VAR=value" to be added to the environment of the process
* @param processId
* A one-element array to which the process ID of the started process will be written.
* @return the file descriptor resulting from opening /dev/ptmx master device. The sub process will have opened the
* slave device counterpart (/dev/pts/$N) and have it as stdint, stdout and stderr.
*/
public static native int createSubprocess(String cmd, String cwd, String[] args, String[] envVars, int[] processId, int rows, int columns);
/** Set the window size for a given pty, which allows connected programs to learn how large their screen is. */
public static native void setPtyWindowSize(int fd, int rows, int cols);
/**
* Causes the calling thread to wait for the process associated with the receiver to finish executing.
*
* @return if >= 0, the exit status of the process. If < 0, the signal causing the process to stop negated.
*/
public static native int waitFor(int processId);
/**
* Send SIGHUP to a process group.
*
* There exists a kill(2) system call wrapper in {@link android.os.Process#sendSignal(int, int)}, but that makes a
* "if (pid > 0)" check so cannot be used for sending to a process group:
* https://android.googlesource.com/platform/frameworks/base/+/donut-release/core/jni/android_util_Process.cpp
*/
public static native void hangupProcessGroup(int processId);
/** Close a file descriptor through the close(2) system call. */
public static native void close(int fileDescriptor);
}

View File

@@ -1,310 +0,0 @@
package com.termux.terminal;
import static android.view.KeyEvent.KEYCODE_BREAK;
import static android.view.KeyEvent.KEYCODE_DEL;
import static android.view.KeyEvent.KEYCODE_DPAD_CENTER;
import static android.view.KeyEvent.KEYCODE_DPAD_DOWN;
import static android.view.KeyEvent.KEYCODE_DPAD_LEFT;
import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT;
import static android.view.KeyEvent.KEYCODE_DPAD_UP;
import static android.view.KeyEvent.KEYCODE_ENTER;
import static android.view.KeyEvent.KEYCODE_ESCAPE;
import static android.view.KeyEvent.KEYCODE_F1;
import static android.view.KeyEvent.KEYCODE_F10;
import static android.view.KeyEvent.KEYCODE_F11;
import static android.view.KeyEvent.KEYCODE_F12;
import static android.view.KeyEvent.KEYCODE_F2;
import static android.view.KeyEvent.KEYCODE_F3;
import static android.view.KeyEvent.KEYCODE_F4;
import static android.view.KeyEvent.KEYCODE_F5;
import static android.view.KeyEvent.KEYCODE_F6;
import static android.view.KeyEvent.KEYCODE_F7;
import static android.view.KeyEvent.KEYCODE_F8;
import static android.view.KeyEvent.KEYCODE_F9;
import static android.view.KeyEvent.KEYCODE_FORWARD_DEL;
import static android.view.KeyEvent.KEYCODE_INSERT;
import static android.view.KeyEvent.KEYCODE_MOVE_END;
import static android.view.KeyEvent.KEYCODE_NUMPAD_0;
import static android.view.KeyEvent.KEYCODE_NUMPAD_1;
import static android.view.KeyEvent.KEYCODE_NUMPAD_2;
import static android.view.KeyEvent.KEYCODE_NUMPAD_3;
import static android.view.KeyEvent.KEYCODE_NUMPAD_4;
import static android.view.KeyEvent.KEYCODE_NUMPAD_5;
import static android.view.KeyEvent.KEYCODE_NUMPAD_6;
import static android.view.KeyEvent.KEYCODE_NUMPAD_7;
import static android.view.KeyEvent.KEYCODE_NUMPAD_8;
import static android.view.KeyEvent.KEYCODE_NUMPAD_9;
import static android.view.KeyEvent.KEYCODE_NUMPAD_ADD;
import static android.view.KeyEvent.KEYCODE_NUMPAD_COMMA;
import static android.view.KeyEvent.KEYCODE_NUMPAD_DIVIDE;
import static android.view.KeyEvent.KEYCODE_NUMPAD_DOT;
import static android.view.KeyEvent.KEYCODE_NUMPAD_ENTER;
import static android.view.KeyEvent.KEYCODE_NUMPAD_EQUALS;
import static android.view.KeyEvent.KEYCODE_NUMPAD_MULTIPLY;
import static android.view.KeyEvent.KEYCODE_NUMPAD_SUBTRACT;
import static android.view.KeyEvent.KEYCODE_NUM_LOCK;
import static android.view.KeyEvent.KEYCODE_PAGE_DOWN;
import static android.view.KeyEvent.KEYCODE_PAGE_UP;
import static android.view.KeyEvent.KEYCODE_SYSRQ;
import static android.view.KeyEvent.KEYCODE_TAB;
import static android.view.KeyEvent.KEYCODE_HOME;
import java.util.HashMap;
import java.util.Map;
import android.view.KeyEvent;
public final class KeyHandler {
public static final int KEYMOD_ALT = 0x80000000;
public static final int KEYMOD_CTRL = 0x40000000;
public static final int KEYMOD_SHIFT = 0x20000000;
private static final Map<String, Integer> TERMCAP_TO_KEYCODE = new HashMap<>();
static {
// terminfo: http://pubs.opengroup.org/onlinepubs/7990989799/xcurses/terminfo.html
// termcap: http://man7.org/linux/man-pages/man5/termcap.5.html
TERMCAP_TO_KEYCODE.put("%i", KEYMOD_SHIFT | KEYCODE_DPAD_RIGHT);
TERMCAP_TO_KEYCODE.put("#2", KEYMOD_SHIFT | KEYCODE_HOME); // Shifted home
TERMCAP_TO_KEYCODE.put("#4", KEYMOD_SHIFT | KEYCODE_DPAD_LEFT);
TERMCAP_TO_KEYCODE.put("*7", KEYMOD_SHIFT | KEYCODE_MOVE_END); // Shifted end key
TERMCAP_TO_KEYCODE.put("k1", KEYCODE_F1);
TERMCAP_TO_KEYCODE.put("k2", KEYCODE_F2);
TERMCAP_TO_KEYCODE.put("k3", KEYCODE_F3);
TERMCAP_TO_KEYCODE.put("k4", KEYCODE_F4);
TERMCAP_TO_KEYCODE.put("k5", KEYCODE_F5);
TERMCAP_TO_KEYCODE.put("k6", KEYCODE_F6);
TERMCAP_TO_KEYCODE.put("k7", KEYCODE_F7);
TERMCAP_TO_KEYCODE.put("k8", KEYCODE_F8);
TERMCAP_TO_KEYCODE.put("k9", KEYCODE_F9);
TERMCAP_TO_KEYCODE.put("k;", KEYCODE_F10);
TERMCAP_TO_KEYCODE.put("F1", KEYCODE_F11);
TERMCAP_TO_KEYCODE.put("F2", KEYCODE_F12);
TERMCAP_TO_KEYCODE.put("F3", KEYMOD_SHIFT | KEYCODE_F1);
TERMCAP_TO_KEYCODE.put("F4", KEYMOD_SHIFT | KEYCODE_F2);
TERMCAP_TO_KEYCODE.put("F5", KEYMOD_SHIFT | KEYCODE_F3);
TERMCAP_TO_KEYCODE.put("F6", KEYMOD_SHIFT | KEYCODE_F4);
TERMCAP_TO_KEYCODE.put("F7", KEYMOD_SHIFT | KEYCODE_F5);
TERMCAP_TO_KEYCODE.put("F8", KEYMOD_SHIFT | KEYCODE_F6);
TERMCAP_TO_KEYCODE.put("F9", KEYMOD_SHIFT | KEYCODE_F7);
TERMCAP_TO_KEYCODE.put("FA", KEYMOD_SHIFT | KEYCODE_F8);
TERMCAP_TO_KEYCODE.put("FB", KEYMOD_SHIFT | KEYCODE_F9);
TERMCAP_TO_KEYCODE.put("FC", KEYMOD_SHIFT | KEYCODE_F10);
TERMCAP_TO_KEYCODE.put("FD", KEYMOD_SHIFT | KEYCODE_F11);
TERMCAP_TO_KEYCODE.put("FE", KEYMOD_SHIFT | KEYCODE_F12);
TERMCAP_TO_KEYCODE.put("kb", KEYCODE_DEL); // backspace key
TERMCAP_TO_KEYCODE.put("kd", KEYCODE_DPAD_DOWN); // terminfo=kcud1, down-arrow key
TERMCAP_TO_KEYCODE.put("kh", KeyEvent.KEYCODE_HOME);
TERMCAP_TO_KEYCODE.put("kl", KEYCODE_DPAD_LEFT);
TERMCAP_TO_KEYCODE.put("kr", KEYCODE_DPAD_RIGHT);
// K1=Upper left of keypad:
// t_K1 <kHome> keypad home key
// t_K3 <kPageUp> keypad page-up key
// t_K4 <kEnd> keypad end key
// t_K5 <kPageDown> keypad page-down key
TERMCAP_TO_KEYCODE.put("K1", KeyEvent.KEYCODE_HOME);
TERMCAP_TO_KEYCODE.put("K3", KeyEvent.KEYCODE_PAGE_UP);
TERMCAP_TO_KEYCODE.put("K4", KeyEvent.KEYCODE_MOVE_END);
TERMCAP_TO_KEYCODE.put("K5", KeyEvent.KEYCODE_PAGE_DOWN);
TERMCAP_TO_KEYCODE.put("ku", KEYCODE_DPAD_UP);
TERMCAP_TO_KEYCODE.put("kB", KEYMOD_SHIFT | KEYCODE_TAB); // termcap=kB, terminfo=kcbt: Back-tab
TERMCAP_TO_KEYCODE.put("kD", KEYCODE_FORWARD_DEL); // terminfo=kdch1, delete-character key
TERMCAP_TO_KEYCODE.put("kDN", KEYMOD_SHIFT | KEYCODE_DPAD_DOWN); // non-standard shifted arrow down
TERMCAP_TO_KEYCODE.put("kF", KEYMOD_SHIFT | KEYCODE_DPAD_DOWN); // terminfo=kind, scroll-forward key
TERMCAP_TO_KEYCODE.put("kI", KEYCODE_INSERT);
TERMCAP_TO_KEYCODE.put("kN", KEYCODE_PAGE_UP);
TERMCAP_TO_KEYCODE.put("kP", KEYCODE_PAGE_DOWN);
TERMCAP_TO_KEYCODE.put("kR", KEYMOD_SHIFT | KEYCODE_DPAD_UP); // terminfo=kri, scroll-backward key
TERMCAP_TO_KEYCODE.put("kUP", KEYMOD_SHIFT | KEYCODE_DPAD_UP); // non-standard shifted up
TERMCAP_TO_KEYCODE.put("@7", KEYCODE_MOVE_END);
TERMCAP_TO_KEYCODE.put("@8", KEYCODE_NUMPAD_ENTER);
}
static String getCodeFromTermcap(String termcap, boolean cursorKeysApplication, boolean keypadApplication) {
Integer keyCodeAndMod = TERMCAP_TO_KEYCODE.get(termcap);
if (keyCodeAndMod == null) return null;
int keyCode = keyCodeAndMod;
int keyMod = 0;
if ((keyCode & KEYMOD_SHIFT) != 0) {
keyMod |= KEYMOD_SHIFT;
keyCode &= ~KEYMOD_SHIFT;
}
if ((keyCode & KEYMOD_CTRL) != 0) {
keyMod |= KEYMOD_CTRL;
keyCode &= ~KEYMOD_CTRL;
}
if ((keyCode & KEYMOD_ALT) != 0) {
keyMod |= KEYMOD_ALT;
keyCode &= ~KEYMOD_ALT;
}
return getCode(keyCode, keyMod, cursorKeysApplication, keypadApplication);
}
public static String getCode(int keyCode, int keyMode, boolean cursorApp, boolean keypadApplication) {
switch (keyCode) {
case KEYCODE_DPAD_CENTER:
return "\015";
case KEYCODE_DPAD_UP:
return (keyMode == 0) ? (cursorApp ? "\033OA" : "\033[A") : transformForModifiers("\033[1", keyMode, 'A');
case KEYCODE_DPAD_DOWN:
return (keyMode == 0) ? (cursorApp ? "\033OB" : "\033[B") : transformForModifiers("\033[1", keyMode, 'B');
case KEYCODE_DPAD_RIGHT:
return (keyMode == 0) ? (cursorApp ? "\033OC" : "\033[C") : transformForModifiers("\033[1", keyMode, 'C');
case KEYCODE_DPAD_LEFT:
return (keyMode == 0) ? (cursorApp ? "\033OD" : "\033[D") : transformForModifiers("\033[1", keyMode, 'D');
case KeyEvent.KEYCODE_HOME:
return (keyMode == 0) ? (cursorApp ? "\033OH" : "\033[H") : transformForModifiers("\033[1", keyMode, 'H');
case KEYCODE_MOVE_END:
return (keyMode == 0) ? (cursorApp ? "\033OF" : "\033[F") : transformForModifiers("\033[1", keyMode, 'F');
// An xterm can send function keys F1 to F4 in two modes: vt100 compatible or
// not. Because Vim may not know what the xterm is sending, both types of keys
// are recognized. The same happens for the <Home> and <End> keys.
// normal vt100 ~
// <F1> t_k1 <Esc>[11~ <xF1> <Esc>OP *<xF1>-xterm*
// <F2> t_k2 <Esc>[12~ <xF2> <Esc>OQ *<xF2>-xterm*
// <F3> t_k3 <Esc>[13~ <xF3> <Esc>OR *<xF3>-xterm*
// <F4> t_k4 <Esc>[14~ <xF4> <Esc>OS *<xF4>-xterm*
// <Home> t_kh <Esc>[7~ <xHome> <Esc>OH *<xHome>-xterm*
// <End> t_@7 <Esc>[4~ <xEnd> <Esc>OF *<xEnd>-xterm*
case KEYCODE_F1:
return (keyMode == 0) ? "\033OP" : transformForModifiers("\033[1", keyMode, 'P');
case KEYCODE_F2:
return (keyMode == 0) ? "\033OQ" : transformForModifiers("\033[1", keyMode, 'Q');
case KEYCODE_F3:
return (keyMode == 0) ? "\033OR" : transformForModifiers("\033[1", keyMode, 'R');
case KEYCODE_F4:
return (keyMode == 0) ? "\033OS" : transformForModifiers("\033[1", keyMode, 'S');
case KEYCODE_F5:
return transformForModifiers("\033[15", keyMode, '~');
case KEYCODE_F6:
return transformForModifiers("\033[17", keyMode, '~');
case KEYCODE_F7:
return transformForModifiers("\033[18", keyMode, '~');
case KEYCODE_F8:
return transformForModifiers("\033[19", keyMode, '~');
case KEYCODE_F9:
return transformForModifiers("\033[20", keyMode, '~');
case KEYCODE_F10:
return transformForModifiers("\033[21", keyMode, '~');
case KEYCODE_F11:
return transformForModifiers("\033[23", keyMode, '~');
case KEYCODE_F12:
return transformForModifiers("\033[24", keyMode, '~');
case KEYCODE_SYSRQ:
return "\033[32~"; // Sys Request / Print
// Is this Scroll lock? case Cancel: return "\033[33~";
case KEYCODE_BREAK:
return "\033[34~"; // Pause/Break
case KEYCODE_ESCAPE:
case KeyEvent.KEYCODE_BACK:
return "\033";
case KEYCODE_INSERT:
return transformForModifiers("\033[2", keyMode, '~');
case KEYCODE_FORWARD_DEL:
return transformForModifiers("\033[3", keyMode, '~');
case KEYCODE_NUMPAD_DOT:
return keypadApplication ? "\033On" : "\033[3~";
case KEYCODE_PAGE_UP:
return "\033[5~";
case KEYCODE_PAGE_DOWN:
return "\033[6~";
case KEYCODE_DEL:
// Yes, this needs to U+007F and not U+0008!
return "\u007F";
case KEYCODE_NUM_LOCK:
return "\033OP";
case KeyEvent.KEYCODE_SPACE:
// If ctrl is not down, return null so that it goes through normal input processing (which may e.g. cause a
// combining accent to be written):
return ((keyMode & KEYMOD_CTRL) == 0) ? null : "\0";
case KEYCODE_TAB:
// This is back-tab when shifted:
return (keyMode & KEYMOD_SHIFT) == 0 ? "\011" : "\033[Z";
case KEYCODE_ENTER:
return ((keyMode & KEYMOD_ALT) == 0) ? "\r" : "\033\r";
case KEYCODE_NUMPAD_ENTER:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'M') : "\n";
case KEYCODE_NUMPAD_MULTIPLY:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'j') : "*";
case KEYCODE_NUMPAD_ADD:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'k') : "+";
case KEYCODE_NUMPAD_COMMA:
return ",";
case KEYCODE_NUMPAD_SUBTRACT:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'm') : "-";
case KEYCODE_NUMPAD_DIVIDE:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'o') : "/";
case KEYCODE_NUMPAD_0:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'p') : "1";
case KEYCODE_NUMPAD_1:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'q') : "1";
case KEYCODE_NUMPAD_2:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'r') : "2";
case KEYCODE_NUMPAD_3:
return keypadApplication ? transformForModifiers("\033O", keyMode, 's') : "3";
case KEYCODE_NUMPAD_4:
return keypadApplication ? transformForModifiers("\033O", keyMode, 't') : "4";
case KEYCODE_NUMPAD_5:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'u') : "5";
case KEYCODE_NUMPAD_6:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'v') : "6";
case KEYCODE_NUMPAD_7:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'w') : "7";
case KEYCODE_NUMPAD_8:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'x') : "8";
case KEYCODE_NUMPAD_9:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'y') : "9";
case KEYCODE_NUMPAD_EQUALS:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'X') : "=";
}
return null;
}
private static String transformForModifiers(String start, int keymod, char lastChar) {
int modifier;
switch (keymod) {
case KEYMOD_SHIFT:
modifier = 2;
break;
case KEYMOD_ALT:
modifier = 3;
break;
case (KEYMOD_SHIFT | KEYMOD_ALT):
modifier = 4;
break;
case KEYMOD_CTRL:
modifier = 5;
break;
case KEYMOD_SHIFT | KEYMOD_CTRL:
modifier = 6;
break;
case KEYMOD_ALT | KEYMOD_CTRL:
modifier = 7;
break;
case KEYMOD_SHIFT | KEYMOD_ALT | KEYMOD_CTRL:
modifier = 8;
break;
default:
return start + lastChar;
}
return start + (";" + modifier) + lastChar;
}
}

View File

@@ -1,443 +0,0 @@
package com.termux.terminal;
/**
* A circular buffer of {@link TerminalRow}:s which keeps notes about what is visible on a logical screen and the scroll
* history.
*
* See {@link #externalToInternalRow(int)} for how to map from logical screen rows to array indices.
*/
public final class TerminalBuffer {
TerminalRow[] mLines;
/** The length of {@link #mLines}. */
int mTotalRows;
/** The number of rows and columns visible on the screen. */
int mScreenRows, mColumns;
/** The number of rows kept in history. */
private int mActiveTranscriptRows = 0;
/** The index in the circular buffer where the visible screen starts. */
private int mScreenFirstRow = 0;
/**
* Create a transcript screen.
*
* @param columns
* the width of the screen in characters.
* @param totalRows
* the height of the entire text area, in rows of text.
* @param screenRows
* the height of just the screen, not including the transcript that holds lines that have scrolled off
* the top of the screen.
*/
public TerminalBuffer(int columns, int totalRows, int screenRows) {
mColumns = columns;
mTotalRows = totalRows;
mScreenRows = screenRows;
mLines = new TerminalRow[totalRows];
blockSet(0, 0, columns, screenRows, ' ', TextStyle.NORMAL);
}
public String getTranscriptText() {
return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows).trim();
}
public String getSelectedText(int selX1, int selY1, int selX2, int selY2) {
final StringBuilder builder = new StringBuilder();
final int columns = mColumns;
if (selY1 < -getActiveTranscriptRows()) selY1 = -getActiveTranscriptRows();
if (selY2 >= mScreenRows) selY2 = mScreenRows - 1;
for (int row = selY1; row <= selY2; row++) {
int x1 = (row == selY1) ? selX1 : 0;
int x2;
if (row == selY2) {
x2 = selX2 + 1;
if (x2 > columns) x2 = columns;
} else {
x2 = columns;
}
TerminalRow lineObject = mLines[externalToInternalRow(row)];
int x1Index = lineObject.findStartOfColumn(x1);
int x2Index = (x2 < mColumns) ? lineObject.findStartOfColumn(x2) : lineObject.getSpaceUsed();
if (x2Index == x1Index) {
// Selected the start of a wide character.
x2Index = lineObject.findStartOfColumn(x2+1);
}
char[] line = lineObject.mText;
int lastPrintingCharIndex = -1;
int i;
boolean rowLineWrap = getLineWrap(row);
if (rowLineWrap && x2 == columns) {
// If the line was wrapped, we shouldn't lose trailing space:
lastPrintingCharIndex = x2Index - 1;
} else {
for (i = x1Index; i < x2Index; ++i) {
char c = line[i];
if (c != ' ') lastPrintingCharIndex = i;
}
}
if (lastPrintingCharIndex != -1) builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1);
if (!rowLineWrap && row < selY2 && row < mScreenRows - 1) builder.append('\n');
}
return builder.toString();
}
public int getActiveTranscriptRows() {
return mActiveTranscriptRows;
}
public int getActiveRows() {
return mActiveTranscriptRows + mScreenRows;
}
/**
* Convert a row value from the public external coordinate system to our internal private coordinate system.
*
* <ul>
* <li>External coordinate system: -mActiveTranscriptRows to mScreenRows-1, with the screen being 0..mScreenRows-1.
* <li>Internal coordinate system: the mScreenRows lines starting at mScreenFirstRow comprise the screen, while the
* mActiveTranscriptRows lines ending at mScreenFirstRow-1 form the transcript (as a circular buffer).
* </ul>
*
* External <---> Internal:
*
* <pre>
* [ ... ] [ ... ]
* [ -mActiveTranscriptRows ] [ mScreenFirstRow - mActiveTranscriptRows ]
* [ ... ] [ ... ]
* [ 0 (visible screen starts here) ] <-----> [ mScreenFirstRow ]
* [ ... ] [ ... ]
* [ mScreenRows-1 ] [ mScreenFirstRow + mScreenRows-1 ]
* </pre>
*
* @param externalRow
* a row in the external coordinate system.
* @return The row corresponding to the input argument in the private coordinate system.
*/
public int externalToInternalRow(int externalRow) {
if (externalRow < -mActiveTranscriptRows || externalRow > mScreenRows)
throw new IllegalArgumentException("extRow=" + externalRow + ", mScreenRows=" + mScreenRows + ", mActiveTranscriptRows=" + mActiveTranscriptRows);
final int internalRow = mScreenFirstRow + externalRow;
return (internalRow < 0) ? (mTotalRows + internalRow) : (internalRow % mTotalRows);
}
public void setLineWrap(int row) {
mLines[externalToInternalRow(row)].mLineWrap = true;
}
public boolean getLineWrap(int row) {
return mLines[externalToInternalRow(row)].mLineWrap;
}
public void clearLineWrap(int row) {
mLines[externalToInternalRow(row)].mLineWrap = false;
}
/**
* Resize the screen which this transcript backs. Currently, this only works if the number of columns does not
* change or the rows expand (that is, it only works when shrinking the number of rows).
*
* @param newColumns
* The number of columns the screen should have.
* @param newRows
* The number of rows the screen should have.
* @param cursor
* An int[2] containing the (column, row) cursor location.
*/
public void resize(int newColumns, int newRows, int newTotalRows, int[] cursor, int currentStyle, boolean altScreen) {
// newRows > mTotalRows should not normally happen since mTotalRows is TRANSCRIPT_ROWS (10000):
if (newColumns == mColumns && newRows <= mTotalRows) {
// Fast resize where just the rows changed.
int shiftDownOfTopRow = mScreenRows - newRows;
if (shiftDownOfTopRow > 0 && shiftDownOfTopRow < mScreenRows) {
// Shrinking. Check if we can skip blank rows at bottom below cursor.
for (int i = mScreenRows - 1; i > 0; i--) {
if (cursor[1] >= i) break;
int r = externalToInternalRow(i);
if (mLines[r] == null || mLines[r].isBlank()) {
if (--shiftDownOfTopRow == 0) break;
}
}
} else if (shiftDownOfTopRow < 0) {
// Negative shift down = expanding. Only move screen up if there is transcript to show:
int actualShift = Math.max(shiftDownOfTopRow, -mActiveTranscriptRows);
if (shiftDownOfTopRow != actualShift) {
// The new lines revealed by the resizing are not all from the transcript. Blank the below ones.
for (int i = 0; i < actualShift - shiftDownOfTopRow; i++)
allocateFullLineIfNecessary((mScreenFirstRow + mScreenRows + i) % mTotalRows).clear(currentStyle);
shiftDownOfTopRow = actualShift;
}
}
mScreenFirstRow += shiftDownOfTopRow;
mScreenFirstRow = (mScreenFirstRow < 0) ? (mScreenFirstRow + mTotalRows) : (mScreenFirstRow % mTotalRows);
mTotalRows = newTotalRows;
mActiveTranscriptRows = altScreen ? 0 : Math.max(0, mActiveTranscriptRows + shiftDownOfTopRow);
cursor[1] -= shiftDownOfTopRow;
mScreenRows = newRows;
} else {
// Copy away old state and update new:
TerminalRow[] oldLines = mLines;
mLines = new TerminalRow[newTotalRows];
for (int i = 0; i < newTotalRows; i++)
mLines[i] = new TerminalRow(newColumns, currentStyle);
final int oldActiveTranscriptRows = mActiveTranscriptRows;
final int oldScreenFirstRow = mScreenFirstRow;
final int oldScreenRows = mScreenRows;
final int oldTotalRows = mTotalRows;
mTotalRows = newTotalRows;
mScreenRows = newRows;
mActiveTranscriptRows = mScreenFirstRow = 0;
mColumns = newColumns;
int newCursorRow = -1;
int newCursorColumn = -1;
int oldCursorRow = cursor[1];
int oldCursorColumn = cursor[0];
boolean newCursorPlaced = false;
int currentOutputExternalRow = 0;
int currentOutputExternalColumn = 0;
// Loop over every character in the initial state.
// Blank lines should be skipped only if at end of transcript (just as is done in the "fast" resize), so we
// keep track how many blank lines we have skipped if we later on find a non-blank line.
int skippedBlankLines = 0;
for (int externalOldRow = -oldActiveTranscriptRows; externalOldRow < oldScreenRows; externalOldRow++) {
// Do what externalToInternalRow() does but for the old state:
int internalOldRow = oldScreenFirstRow + externalOldRow;
internalOldRow = (internalOldRow < 0) ? (oldTotalRows + internalOldRow) : (internalOldRow % oldTotalRows);
TerminalRow oldLine = oldLines[internalOldRow];
boolean cursorAtThisRow = externalOldRow == oldCursorRow;
// The cursor may only be on a non-null line, which we should not skip:
if (oldLine == null || (!(!newCursorPlaced && cursorAtThisRow)) && oldLine.isBlank()) {
skippedBlankLines++;
continue;
} else if (skippedBlankLines > 0) {
// After skipping some blank lines we encounter a non-blank line. Insert the skipped blank lines.
for (int i = 0; i < skippedBlankLines; i++) {
if (currentOutputExternalRow == mScreenRows - 1) {
scrollDownOneLine(0, mScreenRows, currentStyle);
} else {
currentOutputExternalRow++;
}
currentOutputExternalColumn = 0;
}
skippedBlankLines = 0;
}
int lastNonSpaceIndex = 0;
boolean justToCursor = false;
if (cursorAtThisRow || oldLine.mLineWrap) {
// Take the whole line, either because of cursor on it, or if line wrapping.
lastNonSpaceIndex = oldLine.getSpaceUsed();
if (cursorAtThisRow) justToCursor = true;
} else {
for (int i = 0; i < oldLine.getSpaceUsed(); i++)
// NEWLY INTRODUCED BUG! Should not index oldLine.mStyle with char indices
if (oldLine.mText[i] != ' '/* || oldLine.mStyle[i] != currentStyle */) lastNonSpaceIndex = i + 1;
}
int currentOldCol = 0;
int styleAtCol = 0;
for (int i = 0; i < lastNonSpaceIndex; i++) {
// Note that looping over java character, not cells.
char c = oldLine.mText[i];
int codePoint = (Character.isHighSurrogate(c)) ? Character.toCodePoint(c, oldLine.mText[++i]) : c;
int displayWidth = WcWidth.width(codePoint);
// Use the last style if this is a zero-width character:
if (displayWidth > 0) styleAtCol = oldLine.getStyle(currentOldCol);
// Line wrap as necessary:
if (currentOutputExternalColumn + displayWidth > mColumns) {
setLineWrap(currentOutputExternalRow);
if (currentOutputExternalRow == mScreenRows - 1) {
if (newCursorPlaced) newCursorRow--;
scrollDownOneLine(0, mScreenRows, currentStyle);
} else {
currentOutputExternalRow++;
}
currentOutputExternalColumn = 0;
}
int offsetDueToCombiningChar = ((displayWidth <= 0 && currentOutputExternalColumn > 0) ? 1 : 0);
int outputColumn = currentOutputExternalColumn - offsetDueToCombiningChar;
setChar(outputColumn, currentOutputExternalRow, codePoint, styleAtCol);
if (displayWidth > 0) {
if (oldCursorRow == externalOldRow && oldCursorColumn == currentOldCol) {
newCursorColumn = currentOutputExternalColumn;
newCursorRow = currentOutputExternalRow;
newCursorPlaced = true;
}
currentOldCol += displayWidth;
currentOutputExternalColumn += displayWidth;
if (justToCursor && newCursorPlaced) break;
}
}
// Old row has been copied. Check if we need to insert newline if old line was not wrapping:
if (externalOldRow != (oldScreenRows - 1) && !oldLine.mLineWrap) {
if (currentOutputExternalRow == mScreenRows - 1) {
if (newCursorPlaced) newCursorRow--;
scrollDownOneLine(0, mScreenRows, currentStyle);
} else {
currentOutputExternalRow++;
}
currentOutputExternalColumn = 0;
}
}
cursor[0] = newCursorColumn;
cursor[1] = newCursorRow;
}
// Handle cursor scrolling off screen:
if (cursor[0] < 0 || cursor[1] < 0) cursor[0] = cursor[1] = 0;
}
/**
* Block copy lines and associated metadata from one location to another in the circular buffer, taking wraparound
* into account.
*
* @param srcInternal
* The first line to be copied.
* @param len
* The number of lines to be copied.
*/
private void blockCopyLinesDown(int srcInternal, int len) {
if (len == 0) return;
int totalRows = mTotalRows;
int start = len - 1;
// Save away line to be overwritten:
TerminalRow lineToBeOverWritten = mLines[(srcInternal + start + 1) % totalRows];
// Do the copy from bottom to top.
for (int i = start; i >= 0; --i)
mLines[(srcInternal + i + 1) % totalRows] = mLines[(srcInternal + i) % totalRows];
// Put back overwritten line, now above the block:
mLines[(srcInternal) % totalRows] = lineToBeOverWritten;
}
/**
* Scroll the screen down one line. To scroll the whole screen of a 24 line screen, the arguments would be (0, 24).
*
* @param topMargin
* First line that is scrolled.
* @param bottomMargin
* One line after the last line that is scrolled.
* @param style
* the style for the newly exposed line.
*/
public void scrollDownOneLine(int topMargin, int bottomMargin, int style) {
if (topMargin > bottomMargin - 1 || topMargin < 0 || bottomMargin > mScreenRows)
throw new IllegalArgumentException("topMargin=" + topMargin + ", bottomMargin=" + bottomMargin + ", mScreenRows=" + mScreenRows);
// Copy the fixed topMargin lines one line down so that they remain on screen in same position:
blockCopyLinesDown(mScreenFirstRow, topMargin);
// Copy the fixed mScreenRows-bottomMargin lines one line down so that they remain on screen in same
// position:
blockCopyLinesDown(externalToInternalRow(bottomMargin), mScreenRows - bottomMargin);
// Update the screen location in the ring buffer:
mScreenFirstRow = (mScreenFirstRow + 1) % mTotalRows;
// Note that the history has grown if not already full:
if (mActiveTranscriptRows < mTotalRows - mScreenRows) mActiveTranscriptRows++;
// Blank the newly revealed line above the bottom margin:
int blankRow = externalToInternalRow(bottomMargin - 1);
if (mLines[blankRow] == null) {
mLines[blankRow] = new TerminalRow(mColumns, style);
} else {
mLines[blankRow].clear(style);
}
}
/**
* Block copy characters from one position in the screen to another. The two positions can overlap. All characters
* of the source and destination must be within the bounds of the screen, or else an InvalidParameterException will
* be thrown.
*
* @param sx
* source X coordinate
* @param sy
* source Y coordinate
* @param w
* width
* @param h
* height
* @param dx
* destination X coordinate
* @param dy
* destination Y coordinate
*/
public void blockCopy(int sx, int sy, int w, int h, int dx, int dy) {
if (w == 0) return;
if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows || dx < 0 || dx + w > mColumns || dy < 0 || dy + h > mScreenRows)
throw new IllegalArgumentException();
boolean copyingUp = sy > dy;
for (int y = 0; y < h; y++) {
int y2 = copyingUp ? y : (h - (y + 1));
TerminalRow sourceRow = allocateFullLineIfNecessary(externalToInternalRow(sy + y2));
allocateFullLineIfNecessary(externalToInternalRow(dy + y2)).copyInterval(sourceRow, sx, sx + w, dx);
}
}
/**
* Block set characters. All characters must be within the bounds of the screen, or else and
* InvalidParemeterException will be thrown. Typically this is called with a "val" argument of 32 to clear a block
* of characters.
*/
public void blockSet(int sx, int sy, int w, int h, int val, int style) {
if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows) {
throw new IllegalArgumentException(
"Illegal arguments! blockSet(" + sx + ", " + sy + ", " + w + ", " + h + ", " + val + ", " + mColumns + ", " + mScreenRows + ")");
}
for (int y = 0; y < h; y++)
for (int x = 0; x < w; x++)
setChar(sx + x, sy + y, val, style);
}
public TerminalRow allocateFullLineIfNecessary(int row) {
return (mLines[row] == null) ? (mLines[row] = new TerminalRow(mColumns, 0)) : mLines[row];
}
public void setChar(int column, int row, int codePoint, int style) {
if (row >= mScreenRows || column >= mColumns)
throw new IllegalArgumentException("row=" + row + ", column=" + column + ", mScreenRows=" + mScreenRows + ", mColumns=" + mColumns);
row = externalToInternalRow(row);
allocateFullLineIfNecessary(row).setChar(column, codePoint, style);
}
public int getStyleAt(int externalRow, int column) {
return allocateFullLineIfNecessary(externalToInternalRow(externalRow)).getStyle(column);
}
/** Support for http://vt100.net/docs/vt510-rm/DECCARA and http://vt100.net/docs/vt510-rm/DECCARA */
public void setOrClearEffect(int bits, boolean setOrClear, boolean reverse, boolean rectangular, int leftMargin, int rightMargin, int top, int left,
int bottom, int right) {
for (int y = top; y < bottom; y++) {
TerminalRow line = mLines[externalToInternalRow(y)];
int startOfLine = (rectangular || y == top) ? left : leftMargin;
int endOfLine = (rectangular || y + 1 == bottom) ? right : rightMargin;
for (int x = startOfLine; x < endOfLine; x++) {
int currentStyle = line.getStyle(x);
int foreColor = TextStyle.decodeForeColor(currentStyle);
int backColor = TextStyle.decodeBackColor(currentStyle);
int effect = TextStyle.decodeEffect(currentStyle);
if (reverse) {
// Clear out the bits to reverse and add them back in reversed:
effect = (effect & ~bits) | (bits & ~effect);
} else if (setOrClear) {
effect |= bits;
} else {
effect &= ~bits;
}
line.mStyle[x] = TextStyle.encode(foreColor, backColor, effect);
}
}
}
}

View File

@@ -1,102 +0,0 @@
package com.termux.terminal;
import java.util.Map;
import java.util.Properties;
/**
* Color scheme for a terminal with default colors, which may be overridden (and then reset) from the shell using
* Operating System Control (OSC) sequences.
*
* @see TerminalColors
*/
public final class TerminalColorScheme {
/** http://upload.wikimedia.org/wikipedia/en/1/15/Xterm_256color_chart.svg, but with blue color brighter. */
private static final int[] DEFAULT_COLORSCHEME = {
// 16 original colors. First 8 are dim.
0xff000000, // black
0xffcd0000, // dim red
0xff00cd00, // dim green
0xffcdcd00, // dim yellow
0xff6495ed, // dim blue
0xffcd00cd, // dim magenta
0xff00cdcd, // dim cyan
0xffe5e5e5, // dim white
// Second 8 are bright:
0xff7f7f7f, // medium grey
0xffff0000, // bright red
0xff00ff00, // bright green
0xffffff00, // bright yellow
0xff5c5cff, // light blue
0xffff00ff, // bright magenta
0xff00ffff, // bright cyan
0xffffffff, // bright white
// 216 color cube, six shades of each color:
0xff000000, 0xff00005f, 0xff000087, 0xff0000af, 0xff0000d7, 0xff0000ff, 0xff005f00, 0xff005f5f, 0xff005f87, 0xff005faf, 0xff005fd7, 0xff005fff,
0xff008700, 0xff00875f, 0xff008787, 0xff0087af, 0xff0087d7, 0xff0087ff, 0xff00af00, 0xff00af5f, 0xff00af87, 0xff00afaf, 0xff00afd7, 0xff00afff,
0xff00d700, 0xff00d75f, 0xff00d787, 0xff00d7af, 0xff00d7d7, 0xff00d7ff, 0xff00ff00, 0xff00ff5f, 0xff00ff87, 0xff00ffaf, 0xff00ffd7, 0xff00ffff,
0xff5f0000, 0xff5f005f, 0xff5f0087, 0xff5f00af, 0xff5f00d7, 0xff5f00ff, 0xff5f5f00, 0xff5f5f5f, 0xff5f5f87, 0xff5f5faf, 0xff5f5fd7, 0xff5f5fff,
0xff5f8700, 0xff5f875f, 0xff5f8787, 0xff5f87af, 0xff5f87d7, 0xff5f87ff, 0xff5faf00, 0xff5faf5f, 0xff5faf87, 0xff5fafaf, 0xff5fafd7, 0xff5fafff,
0xff5fd700, 0xff5fd75f, 0xff5fd787, 0xff5fd7af, 0xff5fd7d7, 0xff5fd7ff, 0xff5fff00, 0xff5fff5f, 0xff5fff87, 0xff5fffaf, 0xff5fffd7, 0xff5fffff,
0xff870000, 0xff87005f, 0xff870087, 0xff8700af, 0xff8700d7, 0xff8700ff, 0xff875f00, 0xff875f5f, 0xff875f87, 0xff875faf, 0xff875fd7, 0xff875fff,
0xff878700, 0xff87875f, 0xff878787, 0xff8787af, 0xff8787d7, 0xff8787ff, 0xff87af00, 0xff87af5f, 0xff87af87, 0xff87afaf, 0xff87afd7, 0xff87afff,
0xff87d700, 0xff87d75f, 0xff87d787, 0xff87d7af, 0xff87d7d7, 0xff87d7ff, 0xff87ff00, 0xff87ff5f, 0xff87ff87, 0xff87ffaf, 0xff87ffd7, 0xff87ffff,
0xffaf0000, 0xffaf005f, 0xffaf0087, 0xffaf00af, 0xffaf00d7, 0xffaf00ff, 0xffaf5f00, 0xffaf5f5f, 0xffaf5f87, 0xffaf5faf, 0xffaf5fd7, 0xffaf5fff,
0xffaf8700, 0xffaf875f, 0xffaf8787, 0xffaf87af, 0xffaf87d7, 0xffaf87ff, 0xffafaf00, 0xffafaf5f, 0xffafaf87, 0xffafafaf, 0xffafafd7, 0xffafafff,
0xffafd700, 0xffafd75f, 0xffafd787, 0xffafd7af, 0xffafd7d7, 0xffafd7ff, 0xffafff00, 0xffafff5f, 0xffafff87, 0xffafffaf, 0xffafffd7, 0xffafffff,
0xffd70000, 0xffd7005f, 0xffd70087, 0xffd700af, 0xffd700d7, 0xffd700ff, 0xffd75f00, 0xffd75f5f, 0xffd75f87, 0xffd75faf, 0xffd75fd7, 0xffd75fff,
0xffd78700, 0xffd7875f, 0xffd78787, 0xffd787af, 0xffd787d7, 0xffd787ff, 0xffd7af00, 0xffd7af5f, 0xffd7af87, 0xffd7afaf, 0xffd7afd7, 0xffd7afff,
0xffd7d700, 0xffd7d75f, 0xffd7d787, 0xffd7d7af, 0xffd7d7d7, 0xffd7d7ff, 0xffd7ff00, 0xffd7ff5f, 0xffd7ff87, 0xffd7ffaf, 0xffd7ffd7, 0xffd7ffff,
0xffff0000, 0xffff005f, 0xffff0087, 0xffff00af, 0xffff00d7, 0xffff00ff, 0xffff5f00, 0xffff5f5f, 0xffff5f87, 0xffff5faf, 0xffff5fd7, 0xffff5fff,
0xffff8700, 0xffff875f, 0xffff8787, 0xffff87af, 0xffff87d7, 0xffff87ff, 0xffffaf00, 0xffffaf5f, 0xffffaf87, 0xffffafaf, 0xffffafd7, 0xffffafff,
0xffffd700, 0xffffd75f, 0xffffd787, 0xffffd7af, 0xffffd7d7, 0xffffd7ff, 0xffffff00, 0xffffff5f, 0xffffff87, 0xffffffaf, 0xffffffd7, 0xffffffff,
// 24 grey scale ramp:
0xff080808, 0xff121212, 0xff1c1c1c, 0xff262626, 0xff303030, 0xff3a3a3a, 0xff444444, 0xff4e4e4e, 0xff585858, 0xff626262, 0xff6c6c6c, 0xff767676,
0xff808080, 0xff8a8a8a, 0xff949494, 0xff9e9e9e, 0xffa8a8a8, 0xffb2b2b2, 0xffbcbcbc, 0xffc6c6c6, 0xffd0d0d0, 0xffdadada, 0xffe4e4e4, 0xffeeeeee,
// COLOR_INDEX_DEFAULT_FOREGROUND, COLOR_INDEX_DEFAULT_BACKGROUND and COLOR_INDEX_DEFAULT_CURSOR:
0xffffffff, 0xff000000, 0xffffffff };
public final int[] mDefaultColors = new int[TextStyle.NUM_INDEXED_COLORS];
public TerminalColorScheme() {
reset();
}
public void reset() {
System.arraycopy(DEFAULT_COLORSCHEME, 0, mDefaultColors, 0, TextStyle.NUM_INDEXED_COLORS);
}
public void updateWith(Properties props) {
reset();
for (Map.Entry<Object, Object> entries : props.entrySet()) {
String key = (String) entries.getKey();
String value = (String) entries.getValue();
int colorIndex;
if (key.equals("foreground")) {
colorIndex = TextStyle.COLOR_INDEX_FOREGROUND;
} else if (key.equals("background")) {
colorIndex = TextStyle.COLOR_INDEX_BACKGROUND;
} else if (key.equals("cursor")) {
colorIndex = TextStyle.COLOR_INDEX_CURSOR;
} else if (key.startsWith("color")) {
try {
colorIndex = Integer.parseInt(key.substring(5));
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid property: '" + key + "'");
}
} else {
throw new IllegalArgumentException("Invalid property: '" + key + "'");
}
int colorValue = TerminalColors.parse(value);
if (colorValue == 0) throw new IllegalArgumentException("Property '" + key + "' has invalid color: '" + value + "'");
mDefaultColors[colorIndex] = colorValue;
}
}
}

View File

@@ -1,76 +0,0 @@
package com.termux.terminal;
/** Current terminal colors (if different from default). */
public final class TerminalColors {
/** Static data - a bit ugly but ok for now. */
public static final TerminalColorScheme COLOR_SCHEME = new TerminalColorScheme();
/**
* The current terminal colors, which are normally set from the color theme, but may be set dynamically with the OSC
* 4 control sequence.
*/
public final int[] mCurrentColors = new int[TextStyle.NUM_INDEXED_COLORS];
/** Create a new instance with default colors from the theme. */
public TerminalColors() {
reset();
}
/** Reset a particular indexed color with the default color from the color theme. */
public void reset(int index) {
mCurrentColors[index] = COLOR_SCHEME.mDefaultColors[index];
}
/** Reset all indexed colors with the default color from the color theme. */
public void reset() {
System.arraycopy(COLOR_SCHEME.mDefaultColors, 0, mCurrentColors, 0, TextStyle.NUM_INDEXED_COLORS);
}
/**
* Parse color according to http://manpages.ubuntu.com/manpages/intrepid/man3/XQueryColor.3.html
*
* Highest bit is set if successful, so return value is 0xFF${R}${G}${B}. Return 0 if failed.
*/
static int parse(String c) {
try {
int skipInitial, skipBetween;
if (c.charAt(0) == '#') {
// #RGB, #RRGGBB, #RRRGGGBBB or #RRRRGGGGBBBB. Most significant bits.
skipInitial = 1;
skipBetween = 0;
} else if (c.startsWith("rgb:")) {
// rgb:<red>/<green>/<blue> where <red>, <green>, <blue> := h | hh | hhh | hhhh. Scaled.
skipInitial = 4;
skipBetween = 1;
} else {
return 0;
}
int charsForColors = c.length() - skipInitial - 2 * skipBetween;
if (charsForColors % 3 != 0) return 0; // Unequal lengths.
int componentLength = charsForColors / 3;
double mult = 255 / (Math.pow(2, componentLength * 4) - 1);
int currentPosition = skipInitial;
String rString = c.substring(currentPosition, currentPosition + componentLength);
currentPosition += componentLength + skipBetween;
String gString = c.substring(currentPosition, currentPosition + componentLength);
currentPosition += componentLength + skipBetween;
String bString = c.substring(currentPosition, currentPosition + componentLength);
int r = (int) (Integer.parseInt(rString, 16) * mult);
int g = (int) (Integer.parseInt(gString, 16) * mult);
int b = (int) (Integer.parseInt(bString, 16) * mult);
return 0xFF << 24 | r << 16 | g << 8 | b;
} catch (NumberFormatException | IndexOutOfBoundsException e) {
return 0;
}
}
/** Try parse a color from a text parameter and into a specified index. */
public void tryParseColor(int intoIndex, String textParameter) {
int c = parse(textParameter);
if (c != 0) mCurrentColors[intoIndex] = c;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +0,0 @@
package com.termux.terminal;
import java.nio.charset.StandardCharsets;
/** A client which receives callbacks from events triggered by feeding input to a {@link TerminalEmulator}. */
public abstract class TerminalOutput {
/** Write a string using the UTF-8 encoding to the terminal client. */
public final void write(String data) {
byte[] bytes = data.getBytes(StandardCharsets.UTF_8);
write(bytes, 0, bytes.length);
}
/** Write bytes to the terminal client. */
public abstract void write(byte[] data, int offset, int count);
/** Notify the terminal client that the terminal title has changed. */
public abstract void titleChanged(String oldTitle, String newTitle);
/** Notify the terminal client that the terminal title has changed. */
public abstract void clipboardText(String text);
/** Notify the terminal client that a bell character (ASCII 7, bell, BEL, \a, ^G)) has been received. */
public abstract void onBell();
}

View File

@@ -1,232 +0,0 @@
package com.termux.terminal;
import java.util.Arrays;
/**
* A row in a terminal, composed of a fixed number of cells.
*
* The text in the row is stored in a char[] array, {@link #mText}, for quick access during rendering.
*/
public final class TerminalRow {
private static final float SPARE_CAPACITY_FACTOR = 1.5f;
/** The number of columns in this terminal row. */
private final int mColumns;
/** The text filling this terminal row. */
public char[] mText;
/** The number of java char:s used in {@link #mText}. */
private short mSpaceUsed;
/** If this row has been line wrapped due to text output at the end of line. */
boolean mLineWrap;
/** The style bits of each cell in the row. See {@link TextStyle}. */
final int[] mStyle;
/** Construct a blank row (containing only whitespace, ' ') with a specified style. */
public TerminalRow(int columns, int style) {
mColumns = columns;
mText = new char[(int) (SPARE_CAPACITY_FACTOR * columns)];
mStyle = new int[columns];
clear(style);
}
/** NOTE: The sourceX2 is exclusive. */
public void copyInterval(TerminalRow line, int sourceX1, int sourceX2, int destinationX) {
final int x1 = line.findStartOfColumn(sourceX1);
final int x2 = line.findStartOfColumn(sourceX2);
boolean startingFromSecondHalfOfWideChar = (sourceX1 > 0 && line.wideDisplayCharacterStartingAt(sourceX1 - 1));
final char[] sourceChars = (this == line) ? Arrays.copyOf(line.mText, line.mText.length) : line.mText;
int latestNonCombiningWidth = 0;
for (int i = x1; i < x2; i++) {
char sourceChar = sourceChars[i];
int codePoint = Character.isHighSurrogate(sourceChar) ? Character.toCodePoint(sourceChar, sourceChars[++i]) : sourceChar;
if (startingFromSecondHalfOfWideChar) {
// Just treat copying second half of wide char as copying whitespace.
codePoint = ' ';
startingFromSecondHalfOfWideChar = false;
}
int w = WcWidth.width(codePoint);
if (w > 0) {
destinationX += latestNonCombiningWidth;
sourceX1 += latestNonCombiningWidth;
latestNonCombiningWidth = w;
}
setChar(destinationX, codePoint, line.getStyle(sourceX1));
}
}
public int getSpaceUsed() {
return mSpaceUsed;
}
/** Note that the column may end of second half of wide character. */
public int findStartOfColumn(int column) {
if (column == mColumns) return getSpaceUsed();
int currentColumn = 0;
int currentCharIndex = 0;
while (true) { // 0<2 1 < 2
int newCharIndex = currentCharIndex;
char c = mText[newCharIndex++]; // cci=1, cci=2
boolean isHigh = Character.isHighSurrogate(c);
int codePoint = isHigh ? Character.toCodePoint(c, mText[newCharIndex++]) : c;
int wcwidth = WcWidth.width(codePoint); // 1, 2
if (wcwidth > 0) {
currentColumn += wcwidth;
if (currentColumn == column) {
while (newCharIndex < mSpaceUsed) {
// Skip combining chars.
if (Character.isHighSurrogate(mText[newCharIndex])) {
if (WcWidth.width(Character.toCodePoint(mText[newCharIndex], mText[newCharIndex + 1])) <= 0) {
newCharIndex += 2;
} else {
break;
}
} else if (WcWidth.width(mText[newCharIndex]) <= 0) {
newCharIndex++;
} else {
break;
}
}
return newCharIndex;
} else if (currentColumn > column) {
// Wide column going past end.
return currentCharIndex;
}
}
currentCharIndex = newCharIndex;
}
}
private boolean wideDisplayCharacterStartingAt(int column) {
for (int currentCharIndex = 0, currentColumn = 0; currentCharIndex < mSpaceUsed;) {
char c = mText[currentCharIndex++];
int codePoint = Character.isHighSurrogate(c) ? Character.toCodePoint(c, mText[currentCharIndex++]) : c;
int wcwidth = WcWidth.width(codePoint);
if (wcwidth > 0) {
if (currentColumn == column && wcwidth == 2) return true;
currentColumn += wcwidth;
if (currentColumn > column) return false;
}
}
return false;
}
public void clear(int style) {
Arrays.fill(mText, ' ');
Arrays.fill(mStyle, style);
mSpaceUsed = (short) mColumns;
}
// https://github.com/steven676/Android-Terminal-Emulator/commit/9a47042620bec87617f0b4f5d50568535668fe26
public void setChar(int columnToSet, int codePoint, int style) {
mStyle[columnToSet] = style;
final int newCodePointDisplayWidth = WcWidth.width(codePoint);
final boolean newIsCombining = newCodePointDisplayWidth <= 0;
boolean wasExtraColForWideChar = (columnToSet > 0) && wideDisplayCharacterStartingAt(columnToSet - 1);
if (newIsCombining) {
// When standing at second half of wide character and inserting combining:
if (wasExtraColForWideChar) columnToSet--;
} else {
// Check if we are overwriting the second half of a wide character starting at the previous column:
if (wasExtraColForWideChar) setChar(columnToSet - 1, ' ', style);
// Check if we are overwriting the first half of a wide character starting at the next column:
boolean overwritingWideCharInNextColumn = newCodePointDisplayWidth == 2 && wideDisplayCharacterStartingAt(columnToSet + 1);
if (overwritingWideCharInNextColumn) setChar(columnToSet + 1, ' ', style);
}
char[] text = mText;
final int oldStartOfColumnIndex = findStartOfColumn(columnToSet);
final int oldCodePointDisplayWidth = WcWidth.width(text, oldStartOfColumnIndex);
// Get the number of elements in the mText array this column uses now
int oldCharactersUsedForColumn;
if (columnToSet + oldCodePointDisplayWidth < mColumns) {
oldCharactersUsedForColumn = findStartOfColumn(columnToSet + oldCodePointDisplayWidth) - oldStartOfColumnIndex;
} else {
// Last character.
oldCharactersUsedForColumn = mSpaceUsed - oldStartOfColumnIndex;
}
// Find how many chars this column will need
int newCharactersUsedForColumn = Character.charCount(codePoint);
if (newIsCombining) {
// Combining characters are added to the contents of the column instead of overwriting them, so that they
// modify the existing contents.
// FIXME: Put a limit of combining characters.
// FIXME: Unassigned characters also get width=0.
newCharactersUsedForColumn += oldCharactersUsedForColumn;
}
int oldNextColumnIndex = oldStartOfColumnIndex + oldCharactersUsedForColumn;
int newNextColumnIndex = oldStartOfColumnIndex + newCharactersUsedForColumn;
final int javaCharDifference = newCharactersUsedForColumn - oldCharactersUsedForColumn;
if (javaCharDifference > 0) {
// Shift the rest of the line right.
int oldCharactersAfterColumn = mSpaceUsed - oldNextColumnIndex;
if (mSpaceUsed + javaCharDifference > text.length) {
// We need to grow the array
char[] newText = new char[text.length + mColumns];
System.arraycopy(text, 0, newText, 0, oldStartOfColumnIndex + oldCharactersUsedForColumn);
System.arraycopy(text, oldNextColumnIndex, newText, newNextColumnIndex, oldCharactersAfterColumn);
mText = text = newText;
} else {
System.arraycopy(text, oldNextColumnIndex, text, newNextColumnIndex, oldCharactersAfterColumn);
}
} else if (javaCharDifference < 0) {
// Shift the rest of the line left.
System.arraycopy(text, oldNextColumnIndex, text, newNextColumnIndex, mSpaceUsed - oldNextColumnIndex);
}
mSpaceUsed += javaCharDifference;
// Store char. A combining character is stored at the end of the existing contents so that it modifies them:
//noinspection ResultOfMethodCallIgnored - since we already now how many java chars is used.
Character.toChars(codePoint, text, oldStartOfColumnIndex + (newIsCombining ? oldCharactersUsedForColumn : 0));
if (oldCodePointDisplayWidth == 2 && newCodePointDisplayWidth == 1) {
// Replace second half of wide char with a space. Which mean that we actually add a ' ' java character.
if (mSpaceUsed + 1 > text.length) {
char[] newText = new char[text.length + mColumns];
System.arraycopy(text, 0, newText, 0, newNextColumnIndex);
System.arraycopy(text, newNextColumnIndex, newText, newNextColumnIndex + 1, mSpaceUsed - newNextColumnIndex);
mText = text = newText;
} else {
System.arraycopy(text, newNextColumnIndex, text, newNextColumnIndex + 1, mSpaceUsed - newNextColumnIndex);
}
text[newNextColumnIndex] = ' ';
++mSpaceUsed;
} else if (oldCodePointDisplayWidth == 1 && newCodePointDisplayWidth == 2) {
if (columnToSet == mColumns - 1) {
throw new IllegalArgumentException("Cannot put wide character in last column");
} else if (columnToSet == mColumns - 2) {
// Truncate the line to the second part of this wide char:
mSpaceUsed = (short) newNextColumnIndex;
} else {
// Overwrite the contents of the next column, which mean we actually remove java characters. Due to the
// check at the beginning of this method we know that we are not overwriting a wide char.
int newNextNextColumnIndex = newNextColumnIndex + (Character.isHighSurrogate(mText[newNextColumnIndex]) ? 2 : 1);
int nextLen = newNextNextColumnIndex - newNextColumnIndex;
// Shift the array leftwards.
System.arraycopy(text, newNextNextColumnIndex, text, newNextColumnIndex, mSpaceUsed - newNextNextColumnIndex);
mSpaceUsed -= nextLen;
}
}
}
boolean isBlank() {
for (int charIndex = 0, charLen = getSpaceUsed(); charIndex < charLen; charIndex++)
if (mText[charIndex] != ' ') return false;
return true;
}
public final int getStyle(int column) {
return mStyle[column];
}
}

View File

@@ -1,328 +0,0 @@
package com.termux.terminal;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import android.annotation.SuppressLint;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
/**
* A terminal session, consisting of a process coupled to a terminal interface.
* <p>
* The subprocess will be executed by the constructor, and when the size is made known by a call to
* {@link #updateSize(int, int)} terminal emulation will begin and threads will be spawned to handle the subprocess I/O.
* All terminal emulation and callback methods will be performed on the main thread.
* <p>
* The child process may be exited forcefully by using the {@link #finishIfRunning()} method.
*
* NOTE: The terminal session may outlive the EmulatorView, so be careful with callbacks!
*/
public final class TerminalSession extends TerminalOutput {
/** Callback to be invoked when a {@link TerminalSession} changes. */
public interface SessionChangedCallback {
void onTextChanged(TerminalSession changedSession);
void onTitleChanged(TerminalSession changedSession);
void onSessionFinished(TerminalSession finishedSession);
void onClipboardText(TerminalSession session, String text);
void onBell(TerminalSession session);
}
private static FileDescriptor wrapFileDescriptor(int fileDescriptor) {
FileDescriptor result = new FileDescriptor();
try {
Field descriptorField;
try {
descriptorField = FileDescriptor.class.getDeclaredField("descriptor");
} catch (NoSuchFieldException e) {
// For desktop java:
descriptorField = FileDescriptor.class.getDeclaredField("fd");
}
descriptorField.setAccessible(true);
descriptorField.set(result, fileDescriptor);
} catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) {
Log.wtf(EmulatorDebug.LOG_TAG, "Error accessing FileDescriptor#descriptor private field", e);
System.exit(1);
}
return result;
}
private static final int MSG_NEW_INPUT = 1;
private static final int MSG_PROCESS_EXITED = 4;
public final String mHandle = UUID.randomUUID().toString();
TerminalEmulator mEmulator;
/**
* A queue written to from a separate thread when the process outputs, and read by main thread to process by
* terminal emulator.
*/
final ByteQueue mProcessToTerminalIOQueue = new ByteQueue(4096);
/**
* A queue written to from the main thread due to user interaction, and read by another thread which forwards by
* writing to the {@link #mTerminalFileDescriptor}.
*/
final ByteQueue mTerminalToProcessIOQueue = new ByteQueue(4096);
/** Buffer to write translate code points into utf8 before writing to mTerminalToProcessIOQueue */
private final byte[] mUtf8InputBuffer = new byte[5];
/** Callback which gets notified when a session finishes or changes title. */
final SessionChangedCallback mChangeCallback;
/** The pid of the shell process. 0 if not started and -1 if finished running. */
int mShellPid;
/** The exit status of the shell process. Only valid if ${@link #mShellPid} is -1. */
int mShellExitStatus;
/**
* The file descriptor referencing the master half of a pseudo-terminal pair, resulting from calling
* {@link JNI#createSubprocess(String, String, String[], String[], int[], int, int)}.
*/
private int mTerminalFileDescriptor;
/** Set by the application for user identification of session, not by terminal. */
public String mSessionName;
@SuppressLint("HandlerLeak")
final Handler mMainThreadHandler = new Handler() {
final byte[] mReceiveBuffer = new byte[4 * 1024];
@Override
public void handleMessage(Message msg) {
if (msg.what == MSG_NEW_INPUT && isRunning()) {
int bytesRead = mProcessToTerminalIOQueue.read(mReceiveBuffer, false);
if (bytesRead > 0) {
mEmulator.append(mReceiveBuffer, bytesRead);
notifyScreenUpdate();
}
} else if (msg.what == MSG_PROCESS_EXITED) {
int exitCode = (Integer) msg.obj;
cleanupResources(exitCode);
mChangeCallback.onSessionFinished(TerminalSession.this);
String exitDescription = "\r\n[Process completed";
if (exitCode > 0) {
// Non-zero process exit.
exitDescription += " with code " + exitCode;
} else if (exitCode < 0) {
// Negated signal.
exitDescription += " with signal " + (-exitCode);
}
exitDescription += " - press Enter to close]";
byte[] bytesToWrite = exitDescription.getBytes(StandardCharsets.UTF_8);
mEmulator.append(bytesToWrite, bytesToWrite.length);
notifyScreenUpdate();
}
}
};
private final String mShellPath;
private final String mCwd;
private final String[] mArgs;
private final String[] mEnv;
public TerminalSession(String shellPath, String cwd, String[] args, String[] env, SessionChangedCallback changeCallback) {
mChangeCallback = changeCallback;
this.mShellPath = shellPath;
this.mCwd = cwd;
this.mArgs = args;
this.mEnv = env;
}
/** Inform the attached pty of the new size and reflow or initialize the emulator. */
public void updateSize(int columns, int rows) {
if (mEmulator == null) {
initializeEmulator(columns, rows);
} else {
JNI.setPtyWindowSize(mTerminalFileDescriptor, rows, columns);
mEmulator.resize(columns, rows);
}
}
/** The terminal title as set through escape sequences or null if none set. */
public String getTitle() {
return (mEmulator == null) ? null : mEmulator.getTitle();
}
/**
* Set the terminal emulator's window size and start terminal emulation.
*
* @param columns
* The number of columns in the terminal window.
* @param rows
* The number of rows in the terminal window.
*/
public void initializeEmulator(int columns, int rows) {
mEmulator = new TerminalEmulator(this, columns, rows, /* transcript= */5000);
int[] processId = new int[1];
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);
mShellPid = processId[0];
final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor);
new Thread("TermSessionInputReader[pid=" + mShellPid + "]") {
@Override
public void run() {
try (InputStream termIn = new FileInputStream(terminalFileDescriptorWrapped)) {
final byte[] buffer = new byte[4096];
while (true) {
int read = termIn.read(buffer);
if (read == -1) return;
if (!mProcessToTerminalIOQueue.write(buffer, 0, read)) return;
mMainThreadHandler.sendEmptyMessage(MSG_NEW_INPUT);
}
} catch (Exception e) {
// Ignore, just shutting down.
} finally {
// Now wait for process exit:
int processExitCode = JNI.waitFor(mShellPid);
mMainThreadHandler.sendMessage(mMainThreadHandler.obtainMessage(MSG_PROCESS_EXITED, processExitCode));
}
}
}.start();
new Thread("TermSessionOutputWriter[pid=" + mShellPid + "]") {
@Override
public void run() {
final byte[] buffer = new byte[4096];
try (FileOutputStream termOut = new FileOutputStream(terminalFileDescriptorWrapped)) {
while (true) {
int bytesToWrite = mTerminalToProcessIOQueue.read(buffer, true);
if (bytesToWrite == -1) return;
termOut.write(buffer, 0, bytesToWrite);
}
} catch (IOException e) {
// Ignore.
}
}
}.start();
}
/** Write data to the shell process. */
@Override
public void write(byte[] data, int offset, int count) {
if (mShellPid > 0) mTerminalToProcessIOQueue.write(data, offset, count);
}
/** Write the Unicode code point to the terminal encoded in UTF-8. */
public void writeCodePoint(boolean prependEscape, int codePoint) {
if (codePoint > 1114111 || (codePoint >= 0xD800 && codePoint <= 0xDFFF)) {
// 1114111 (= 2**16 + 1024**2 - 1) is the highest code point, [0xD800,0xDFFF] is the surrogate range.
throw new IllegalArgumentException("Invalid code point: " + codePoint);
}
int bufferPosition = 0;
if (prependEscape) mUtf8InputBuffer[bufferPosition++] = 27;
if (codePoint <= /* 7 bits */0b1111111) {
mUtf8InputBuffer[bufferPosition++] = (byte) codePoint;
} else if (codePoint <= /* 11 bits */0b11111111111) {
/* 110xxxxx leading byte with leading 5 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11000000 | (codePoint >> 6));
/* 10xxxxxx continuation byte with following 6 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
} else if (codePoint <= /* 16 bits */0b1111111111111111) {
/* 1110xxxx leading byte with leading 4 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11100000 | (codePoint >> 12));
/* 10xxxxxx continuation byte with following 6 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 6) & 0b111111));
/* 10xxxxxx continuation byte with following 6 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
} else { /* We have checked codePoint <= 1114111 above, so we have max 21 bits = 0b111111111111111111111 */
/* 11110xxx leading byte with leading 3 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11110000 | (codePoint >> 18));
/* 10xxxxxx continuation byte with following 6 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 12) & 0b111111));
/* 10xxxxxx continuation byte with following 6 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 6) & 0b111111));
/* 10xxxxxx continuation byte with following 6 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
}
write(mUtf8InputBuffer, 0, bufferPosition);
}
public TerminalEmulator getEmulator() {
return mEmulator;
}
/** Notify the {@link #mChangeCallback} that the screen has changed. */
protected void notifyScreenUpdate() {
mChangeCallback.onTextChanged(this);
}
/** Reset state for terminal emulator state. */
public void reset() {
mEmulator.reset();
notifyScreenUpdate();
}
/**
* Finish this terminal session. Frees resources used by the terminal emulator and closes the attached
* <code>InputStream</code> and <code>OutputStream</code>.
*/
public void finishIfRunning() {
if (isRunning()) {
JNI.hangupProcessGroup(mShellPid);
// Stop the reader and writer threads, and close the I/O streams. Note that
// cleanupResources() will be run later.
mTerminalToProcessIOQueue.close();
mProcessToTerminalIOQueue.close();
JNI.close(mTerminalFileDescriptor);
}
}
/** Cleanup resources when the process exits. */
void cleanupResources(int exitStatus) {
synchronized (this) {
mShellPid = -1;
mShellExitStatus = exitStatus;
}
// Stop the reader and writer threads, and close the I/O streams
mTerminalToProcessIOQueue.close();
mProcessToTerminalIOQueue.close();
JNI.close(mTerminalFileDescriptor);
}
@Override
public void titleChanged(String oldTitle, String newTitle) {
mChangeCallback.onTitleChanged(this);
}
public synchronized boolean isRunning() {
return mShellPid != -1;
}
/** Only valid if not {@link #isRunning()}. */
public synchronized int getExitStatus() {
return mShellExitStatus;
}
@Override
public void clipboardText(String text) {
mChangeCallback.onClipboardText(this, text);
}
@Override
public void onBell() {
mChangeCallback.onBell(this);
}
}

View File

@@ -1,55 +0,0 @@
package com.termux.terminal;
/**
* Encodes effects, foreground and background colors into a 32 bit integer, which are stored for each cell in a terminal
* row in {@link TerminalRow#mStyle}.
*
* The foreground and background colors take 9 bits each, leaving (32-9-9)=14 bits for effect flags. Using 9 for now
* (the different CHARACTER_ATTRIBUTE_* bits).
*/
public final class TextStyle {
public final static int CHARACTER_ATTRIBUTE_BOLD = 1;
public final static int CHARACTER_ATTRIBUTE_ITALIC = 1 << 1;
public final static int CHARACTER_ATTRIBUTE_UNDERLINE = 1 << 2;
public final static int CHARACTER_ATTRIBUTE_BLINK = 1 << 3;
public final static int CHARACTER_ATTRIBUTE_INVERSE = 1 << 4;
public final static int CHARACTER_ATTRIBUTE_INVISIBLE = 1 << 5;
public final static int CHARACTER_ATTRIBUTE_STRIKETHROUGH = 1 << 6;
/**
* The selective erase control functions (DECSED and DECSEL) can only erase characters defined as erasable.
*
* This bit is set if DECSCA (Select Character Protection Attribute) has been used to define the characters that
* come after it as erasable from the screen.
*/
public final static int CHARACTER_ATTRIBUTE_PROTECTED = 1 << 7;
/** Dim colors. Also known as faint or half intensity. */
public final static int CHARACTER_ATTRIBUTE_DIM = 1 << 8;
public final static int COLOR_INDEX_FOREGROUND = 256;
public final static int COLOR_INDEX_BACKGROUND = 257;
public final static int COLOR_INDEX_CURSOR = 258;
/** The 256 standard color entries and the three special (foreground, background and cursor) ones. */
public final static int NUM_INDEXED_COLORS = 259;
/** Normal foreground and background colors and no effects. */
final static int NORMAL = encode(COLOR_INDEX_FOREGROUND, COLOR_INDEX_BACKGROUND, 0);
static int encode(int foreColor, int backColor, int effect) {
return ((effect & 0b111111111) << 18) | ((foreColor & 0b111111111) << 9) | (backColor & 0b111111111);
}
public static int decodeForeColor(int encodedColor) {
return (encodedColor >> 9) & 0b111111111;
}
public static int decodeBackColor(int encodedColor) {
return encodedColor & 0b111111111;
}
public static int decodeEffect(int encodedColor) {
return (encodedColor >> 18) & 0b111111111;
}
}

View File

@@ -1,108 +0,0 @@
package com.termux.terminal;
/**
* wcwidth() implementation from http://git.musl-libc.org/cgit/musl/tree/src/ctype
*
* Modified to return 0 instead of -1.
*/
public final class WcWidth {
private static final short table[] = { 16, 16, 16, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 16, 16, 32, 16, 16, 16, 33, 34, 35, 36, 37, 38,
39, 16, 16, 40, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 41, 42, 16, 16, 43, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 44, 16, 45, 46, 47, 48, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
49, 16, 16, 50, 51, 16, 52, 16, 16, 16, 16, 16, 16, 16, 16, 53, 16, 16, 16, 16, 16, 54, 55, 16, 16, 16, 16, 56, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 57, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 58, 59, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
248, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 254, 255, 255, 255, 255, 191, 182, 0, 0, 0,
0, 0, 0, 0, 31, 0, 255, 7, 0, 0, 0, 0, 0, 248, 255, 255, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 191, 159, 61, 0, 0, 0, 128, 2, 0, 0, 0,
255, 255, 255, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 255, 1, 0, 0, 0, 0, 0, 0, 248, 15, 0, 0, 0, 192, 251, 239, 62, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 240, 255, 255, 127, 7, 0, 0, 0, 0, 0, 0, 20, 254, 33, 254, 0, 12, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 16, 30, 32, 0,
0, 12, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 16, 134, 57, 2, 0, 0, 0, 35, 0, 6, 0, 0, 0, 0, 0, 0, 16, 190, 33, 0, 0, 12, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 144,
30, 32, 64, 0, 12, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 1, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 193, 61, 96, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 144, 64, 48, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 32, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 92, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 242, 7, 128, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 242, 27, 0, 63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 160, 2, 0, 0, 0, 0, 0, 0, 254,
127, 223, 224, 255, 254, 255, 255, 255, 31, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 253, 102, 0, 0, 0, 195, 1, 0, 30, 0, 100, 32, 0, 32, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0,
28, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 176, 63, 64, 254, 15, 32, 0, 0, 0, 0, 0, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 135, 1, 4, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
128, 1, 0, 0, 0, 0, 0, 0, 64, 127, 229, 31, 248, 159, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 208, 23, 4, 0, 0, 0, 0,
248, 15, 0, 3, 0, 0, 0, 60, 11, 0, 0, 0, 0, 0, 0, 64, 163, 3, 0, 0, 0, 0, 0, 0, 240, 207, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
247, 255, 253, 33, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 127, 0, 0, 240, 0, 248, 0, 0,
0, 124, 0, 0, 0, 0, 0, 0, 31, 252, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255,
255, 0, 0, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128,
247, 63, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 68, 8, 0, 0, 96, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0,
255, 255, 3, 0, 0, 0, 0, 0, 192, 63, 0, 0, 128, 255, 3, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 200, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 126, 102,
0, 8, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 157, 193, 2, 0, 0, 0, 0, 48, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 32, 33, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0,
127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 110, 240, 0,
0, 0, 0, 0, 135, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 255, 127, 0, 0, 0, 0, 0, 0, 0, 3, 0,
0, 0, 0, 0, 120, 38, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 128, 239, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 192, 127, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40, 191, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 128, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 3, 248, 255, 231, 15, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };
private static final short wtable[] = { 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 18, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 19, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 20, 21, 22, 23, 24, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 25, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 26, 16, 16, 16, 16, 27, 16, 16, 17, 17, 17, 17, 17,
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
17, 28, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 17,
16, 16, 16, 29, 30, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 31, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 32, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 248, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 252, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 251, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 15, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 63, 0, 0, 0, 255, 15, 255, 255, 255, 255, 255, 255, 255, 127, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127, 254, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 224, 255, 255, 255, 255, 63, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127, 255, 255,
255, 255, 255, 7, 255, 255, 255, 255, 15, 0, 255, 255, 255, 255, 255, 127, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 31, 255, 255, 255, 255, 255, 255, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 15, 0, 255, 255, 127, 248,
255, 255, 255, 255, 255, 15, 0, 0, 255, 3, 0, 0, 255, 255, 255, 255, 247, 255, 127, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 254,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 127, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 255, 255, 255, 255, 255, 7, 255, 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0 };
/** Return the terminal display width of a code point: 0, 1 or 2. */
public static int width(int wc) {
if (wc < 0xff) return (wc + 1 & 0x7f) >= 0x21 ? 1 : (wc != 0) ? 0 : 0;
if ((wc & 0xfffeffff) < 0xfffe) {
if (((table[table[wc >> 8] * 32 + ((wc & 255) >> 3)] >> (wc & 7)) & 1) != 0) return 0;
if (((wtable[wtable[wc >> 8] * 32 + ((wc & 255) >> 3)] >> (wc & 7)) & 1) != 0) return 2;
return 1;
}
if ((wc & 0xfffe) == 0xfffe) return 0;
if (wc - 0x20000 < 0x20000) return 2;
if (wc == 0xe0001 || wc - 0xe0020 < 0x5f || wc - 0xe0100 < 0xef) return 0;
return 1;
}
/** The width at an index position in a java char array. */
public static int width(char[] chars, int index) {
char c = chars[index];
return Character.isHighSurrogate(c) ? width(Character.toCodePoint(c, chars[index + 1])) : width(c);
}
}

View File

@@ -1,111 +0,0 @@
package com.termux.view;
import android.content.Context;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
/** A combination of {@link GestureDetector} and {@link ScaleGestureDetector}. */
public final class GestureAndScaleRecognizer {
public interface Listener {
boolean onSingleTapUp(MotionEvent e);
boolean onDoubleTap(MotionEvent e);
boolean onScroll(MotionEvent e2, float dx, float dy);
boolean onFling(MotionEvent e, float velocityX, float velocityY);
boolean onScale(float focusX, float focusY, float scale);
boolean onDown(float x, float y);
boolean onUp(MotionEvent e);
void onLongPress(MotionEvent e);
}
private final GestureDetector mGestureDetector;
private final ScaleGestureDetector mScaleDetector;
final Listener mListener;
boolean isAfterLongPress;
public GestureAndScaleRecognizer(Context context, Listener listener) {
mListener = listener;
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx, float dy) {
return mListener.onScroll(e2, dx, dy);
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return mListener.onFling(e2, velocityX, velocityY);
}
@Override
public boolean onDown(MotionEvent e) {
return mListener.onDown(e.getX(), e.getY());
}
@Override
public void onLongPress(MotionEvent e) {
mListener.onLongPress(e);
isAfterLongPress = true;
}
}, null, true /* ignoreMultitouch */);
mGestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
return mListener.onSingleTapUp(e);
}
@Override
public boolean onDoubleTap(MotionEvent e) {
return mListener.onDoubleTap(e);
}
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
return true;
}
});
mScaleDetector = new ScaleGestureDetector(context, new ScaleGestureDetector.SimpleOnScaleGestureListener() {
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
return true;
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
return mListener.onScale(detector.getFocusX(), detector.getFocusY(), detector.getScaleFactor());
}
});
}
public void onTouchEvent(MotionEvent event) {
mGestureDetector.onTouchEvent(event);
mScaleDetector.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
isAfterLongPress = false;
break;
case MotionEvent.ACTION_UP:
if (!isAfterLongPress) {
// This behaviour is desired when in e.g. vim with mouse events, where we do not
// want to move the cursor when lifting finger after a long press.
mListener.onUp(event);
}
break;
}
}
public boolean isInProgress() {
return mScaleDetector.isInProgress();
}
}

View File

@@ -1,24 +0,0 @@
package com.termux.view;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
/**
* Input and scale listener which may be set on a {@link TerminalView} through
* {@link TerminalView#setOnKeyListener(TerminalKeyListener)}.
*
* TODO: Rename to TerminalViewClient.
*/
public interface TerminalKeyListener {
/** Callback function on scale events according to {@link ScaleGestureDetector#getScaleFactor()}. */
float onScale(float scale);
/** On a single tap on the terminal if terminal mouse reporting not enabled. */
void onSingleTapUp(MotionEvent e);
boolean shouldBackButtonBeMappedToEscape();
void copyModeChanged(boolean copyMode);
}

View File

@@ -1,236 +0,0 @@
package com.termux.view;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.Typeface;
import com.termux.terminal.TerminalBuffer;
import com.termux.terminal.TerminalEmulator;
import com.termux.terminal.TerminalRow;
import com.termux.terminal.TextStyle;
import com.termux.terminal.WcWidth;
/**
* Renderer of a {@link TerminalEmulator} into a {@link Canvas}.
*
* Saves font metrics, so needs to be recreated each time the typeface or font size changes.
*/
final class TerminalRenderer {
final int mTextSize;
final Typeface mTypeface;
private final Paint mTextPaint = new Paint();
/** The width of a single mono spaced character obtained by {@link Paint#measureText(String)} on a single 'X'. */
final float mFontWidth;
/** The {@link Paint#getFontSpacing()}. See http://www.fampennings.nl/maarten/android/08numgrid/font.png */
final int mFontLineSpacing;
/** The {@link Paint#ascent()}. See http://www.fampennings.nl/maarten/android/08numgrid/font.png */
private final int mFontAscent;
/** The {@link #mFontLineSpacing} + {@link #mFontAscent}. */
final int mFontLineSpacingAndAscent;
private final float[] asciiMeasures = new float[127];
public TerminalRenderer(int textSize, Typeface typeface) {
mTextSize = textSize;
mTypeface = typeface;
mTextPaint.setTypeface(typeface);
mTextPaint.setAntiAlias(true);
mTextPaint.setTextSize(textSize);
mFontLineSpacing = (int) Math.ceil(mTextPaint.getFontSpacing());
mFontAscent = (int) Math.ceil(mTextPaint.ascent());
mFontLineSpacingAndAscent = mFontLineSpacing + mFontAscent;
mFontWidth = mTextPaint.measureText("X");
StringBuilder sb = new StringBuilder(" ");
for (int i = 0; i < asciiMeasures.length; i++) {
sb.setCharAt(0, (char) i);
asciiMeasures[i] = mTextPaint.measureText(sb, 0, 1);
}
}
/** Render the terminal to a canvas with at a specified row scroll, and an optional rectangular selection. */
public final void render(TerminalEmulator mEmulator, Canvas canvas, int topRow, int selectionY1, int selectionY2, int selectionX1, int selectionX2) {
final boolean reverseVideo = mEmulator.isReverseVideo();
final int endRow = topRow + mEmulator.mRows;
final int columns = mEmulator.mColumns;
final int cursorCol = mEmulator.getCursorCol();
final int cursorRow = mEmulator.getCursorRow();
final boolean cursorVisible = mEmulator.isShowingCursor();
final TerminalBuffer screen = mEmulator.getScreen();
final int[] palette = mEmulator.mColors.mCurrentColors;
int fillColor = palette[reverseVideo ? TextStyle.COLOR_INDEX_FOREGROUND : TextStyle.COLOR_INDEX_BACKGROUND];
canvas.drawColor(fillColor, PorterDuff.Mode.SRC);
float heightOffset = mFontLineSpacingAndAscent;
for (int row = topRow; row < endRow; row++) {
heightOffset += mFontLineSpacing;
final int cursorX = (row == cursorRow && cursorVisible) ? cursorCol : -1;
int selx1 = -1, selx2 = -1;
if (row >= selectionY1 && row <= selectionY2) {
if (row == selectionY1) selx1 = selectionX1;
selx2 = (row == selectionY2) ? selectionX2 : mEmulator.mColumns;
}
TerminalRow lineObject = screen.allocateFullLineIfNecessary(screen.externalToInternalRow(row));
final char[] line = lineObject.mText;
final int charsUsedInLine = lineObject.getSpaceUsed();
int lastRunStyle = 0;
boolean lastRunInsideCursor = false;
int lastRunStartColumn = -1;
int lastRunStartIndex = 0;
boolean lastRunFontWidthMismatch = false;
int currentCharIndex = 0;
float measuredWidthForRun = 0.f;
for (int column = 0; column < columns;) {
final char charAtIndex = line[currentCharIndex];
final boolean charIsHighsurrogate = Character.isHighSurrogate(charAtIndex);
final int charsForCodePoint = charIsHighsurrogate ? 2 : 1;
final int codePoint = charIsHighsurrogate ? Character.toCodePoint(charAtIndex, line[currentCharIndex + 1]) : charAtIndex;
final int codePointWcWidth = WcWidth.width(codePoint);
final boolean insideCursor = (column >= selx1 && column <= selx2) || (cursorX == column || (codePointWcWidth == 2 && cursorX == column + 1));
final int style = lineObject.getStyle(column);
// Check if the measured text width for this code point is not the same as that expected by wcwidth().
// This could happen for some fonts which are not truly monospace, or for more exotic characters such as
// smileys which android font renders as wide.
// If this is detected, we draw this code point scaled to match what wcwidth() expects.
final float measuredCodePointWidth = (codePoint < asciiMeasures.length) ? asciiMeasures[codePoint] : mTextPaint.measureText(line,
currentCharIndex, charsForCodePoint);
final boolean fontWidthMismatch = Math.abs(measuredCodePointWidth / mFontWidth - codePointWcWidth) > 0.01;
if (style != lastRunStyle || insideCursor != lastRunInsideCursor || fontWidthMismatch || lastRunFontWidthMismatch) {
if (column == 0) {
// Skip first column as there is nothing to draw, just record the current style.
} else {
final int columnWidthSinceLastRun = column - lastRunStartColumn;
final int charsSinceLastRun = currentCharIndex - lastRunStartIndex;
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun,
measuredWidthForRun, lastRunInsideCursor, lastRunStyle, reverseVideo);
}
measuredWidthForRun = 0.f;
lastRunStyle = style;
lastRunInsideCursor = insideCursor;
lastRunStartColumn = column;
lastRunStartIndex = currentCharIndex;
lastRunFontWidthMismatch = fontWidthMismatch;
}
measuredWidthForRun += measuredCodePointWidth;
column += codePointWcWidth;
currentCharIndex += charsForCodePoint;
while (currentCharIndex < charsUsedInLine && WcWidth.width(line, currentCharIndex) <= 0) {
// Eat combining chars so that they are treated as part of the last non-combining code point,
// instead of e.g. being considered inside the cursor in the next run.
currentCharIndex += Character.isHighSurrogate(line[currentCharIndex]) ? 2 : 1;
}
}
final int columnWidthSinceLastRun = columns - lastRunStartColumn;
final int charsSinceLastRun = currentCharIndex - lastRunStartIndex;
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun,
measuredWidthForRun, lastRunInsideCursor, lastRunStyle, reverseVideo);
}
}
/**
* @param canvas
* the canvas to render on
* @param palette
* the color palette to look up colors from textStyle
* @param y
* height offset into the canvas where to render the line: line * {@link #mFontLineSpacing}
* @param startColumn
* the run offset in columns
* @param runWidthColumns
* the run width in columns - this is computed from wcwidth() and may not be what the font measures to
* @param text
* the java char array to render text from
* @param startCharIndex
* index into the text array where to start
* @param runWidthChars
* number of java characters from the text array to render
* @param cursor
* true if rendering a cursor or selection
* @param textStyle
* the background, foreground and effect encoded using {@link TextStyle}
* @param reverseVideo
* if the screen is rendered with the global reverse video flag set
*/
private void drawTextRun(Canvas canvas, char[] text, int[] palette, float y, int startColumn, int runWidthColumns, int startCharIndex, int runWidthChars,
float mes, boolean cursor, int textStyle, boolean reverseVideo) {
int foreColor = TextStyle.decodeForeColor(textStyle);
int backColor = TextStyle.decodeBackColor(textStyle);
final int effect = TextStyle.decodeEffect(textStyle);
float left = startColumn * mFontWidth;
float right = left + runWidthColumns * mFontWidth;
mes = mes / mFontWidth;
boolean savedMatrix = false;
if (Math.abs(mes - runWidthColumns) > 0.01) {
canvas.save();
canvas.scale(runWidthColumns / mes, 1.f);
left *= mes / runWidthColumns;
right *= mes / runWidthColumns;
savedMatrix = true;
}
// Reverse video here if _one and only one_ of the reverse flags are set:
boolean reverseVideoHere = reverseVideo ^ (effect & (TextStyle.CHARACTER_ATTRIBUTE_INVERSE)) != 0;
// Switch if _one and only one_ of reverse video and cursor is set:
if (reverseVideoHere ^ cursor) {
int tmp = foreColor;
foreColor = backColor;
backColor = tmp;
}
if (backColor != TextStyle.COLOR_INDEX_BACKGROUND) {
// Only draw non-default background.
mTextPaint.setColor(palette[backColor]);
canvas.drawRect(left, y - mFontLineSpacingAndAscent + mFontAscent, right, y, mTextPaint);
}
if ((effect & TextStyle.CHARACTER_ATTRIBUTE_INVISIBLE) == 0) {
// Treat blink as bold:
final boolean bold = (effect & (TextStyle.CHARACTER_ATTRIBUTE_BOLD | TextStyle.CHARACTER_ATTRIBUTE_BLINK)) != 0;
final boolean underline = (effect & TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE) != 0;
final boolean italic = (effect & TextStyle.CHARACTER_ATTRIBUTE_ITALIC) != 0;
final boolean strikeThrough = (effect & TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH) != 0;
final boolean dim = (effect & TextStyle.CHARACTER_ATTRIBUTE_DIM) != 0;
// Let bold have bright colors if applicable (one of the first 8):
final int actualForeColor = foreColor + (bold && foreColor < 8 ? 8 : 0);
int foreColorARGB = palette[actualForeColor];
if (dim) {
int red = (0xFF & (foreColorARGB >> 16));
int green = (0xFF & (foreColorARGB >> 8));
int blue = (0xFF & foreColorARGB);
// Dim color handling used by libvte which in turn took it from xterm
// (https://bug735245.bugzilla-attachments.gnome.org/attachment.cgi?id=284267):
red = red * 2 / 3;
green = green * 2 / 3;
blue = blue * 2 / 3;
foreColorARGB = 0xFF000000 + (red << 16) + (green << 8) + blue;
}
mTextPaint.setFakeBoldText(bold);
mTextPaint.setUnderlineText(underline);
mTextPaint.setTextSkewX(italic ? -0.35f : 0.f);
mTextPaint.setStrikeThruText(strikeThrough);
mTextPaint.setColor(foreColorARGB);
// The text alignment is the default Paint.Align.LEFT.
canvas.drawText(text, startCharIndex, runWidthChars, left, y - mFontLineSpacingAndAscent, mTextPaint);
}
if (savedMatrix) canvas.restore();
}
}

View File

@@ -1,995 +0,0 @@
package com.termux.view;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.media.AudioManager;
import android.os.Build;
import android.text.InputType;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ActionMode;
import android.view.HapticFeedbackConstants;
import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.Scroller;
import com.termux.R;
import com.termux.terminal.EmulatorDebug;
import com.termux.terminal.KeyHandler;
import com.termux.terminal.TerminalBuffer;
import com.termux.terminal.TerminalColors;
import com.termux.terminal.TerminalEmulator;
import com.termux.terminal.TerminalSession;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Properties;
/** View displaying and interacting with a {@link TerminalSession}. */
public final class TerminalView extends View {
/** Log view key and IME events. */
private static final boolean LOG_KEY_EVENTS = false;
/** The currently displayed terminal session, whose emulator is {@link #mEmulator}. */
TerminalSession mTermSession;
/** Our terminal emulator whose session is {@link #mTermSession}. */
TerminalEmulator mEmulator;
TerminalRenderer mRenderer;
TerminalKeyListener mOnKeyListener;
/** The top row of text to display. Ranges from -activeTranscriptRows to 0. */
int mTopRow;
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
boolean mIsSelectingText = false, mIsDraggingLeftSelection, mInitialTextSelection;
int mSelX1 = -1, mSelX2 = -1, mSelY1 = -1, mSelY2 = -1;
float mSelectionDownX, mSelectionDownY;
private ActionMode mActionMode;
private BitmapDrawable mLeftSelectionHandle, mRightSelectionHandle;
float mScaleFactor = 1.f;
final GestureAndScaleRecognizer mGestureRecognizer;
/** Keep track of where mouse touch event started which we report as mouse scroll. */
private int mMouseScrollStartX = -1, mMouseScrollStartY = -1;
/** Keep track of the time when a touch event leading to sending mouse scroll events started. */
private long mMouseStartDownTime = -1;
final Scroller mScroller;
/** What was left in from scrolling movement. */
float mScrollRemainder;
/** If non-zero, this is the last unicode code point received if that was a combining character. */
int mCombiningAccent;
public TerminalView(Context context, AttributeSet attributes) { // NO_UCD (unused code)
super(context, attributes);
mGestureRecognizer = new GestureAndScaleRecognizer(context, new GestureAndScaleRecognizer.Listener() {
@Override
public boolean onUp(MotionEvent e) {
mScrollRemainder = 0.0f;
if (mEmulator != null && mEmulator.isMouseTrackingActive() && !mIsSelectingText) {
// Quick event processing when mouse tracking is active - do not wait for check of double tapping
// for zooming.
sendMouseEventCode(e, TerminalEmulator.MOUSE_LEFT_BUTTON, true);
sendMouseEventCode(e, TerminalEmulator.MOUSE_LEFT_BUTTON, false);
return true;
}
return false;
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
if (mEmulator == null) return true;
if (mIsSelectingText) { toggleSelectingText(null); return true; }
requestFocus();
if (!mEmulator.isMouseTrackingActive()) {
if (!e.isFromSource(InputDevice.SOURCE_MOUSE)) {
mOnKeyListener.onSingleTapUp(e);
return true;
}
}
return false;
}
@Override
public boolean onScroll(MotionEvent e2, float distanceX, float distanceY) {
if (mEmulator == null || mIsSelectingText) return true;
if (mEmulator.isMouseTrackingActive() && e2.isFromSource(InputDevice.SOURCE_MOUSE)) {
// If moving with mouse pointer while pressing button, report that instead of scroll.
// This means that we never report moving with button press-events for touch input,
// since we cannot just start sending these events without a starting press event,
// which we do not do for touch input, only mouse in onTouchEvent().
sendMouseEventCode(e2, TerminalEmulator.MOUSE_LEFT_BUTTON_MOVED, true);
} else {
distanceY += mScrollRemainder;
int deltaRows = (int) (distanceY / mRenderer.mFontLineSpacing);
mScrollRemainder = distanceY - deltaRows * mRenderer.mFontLineSpacing;
doScroll(e2, deltaRows);
}
return true;
}
@Override
public boolean onScale(float focusX, float focusY, float scale) {
if (mEmulator == null || mIsSelectingText) return true;
mScaleFactor *= scale;
mScaleFactor = mOnKeyListener.onScale(mScaleFactor);
return true;
}
@Override
public boolean onFling(final MotionEvent e2, float velocityX, float velocityY) {
if (mEmulator == null || mIsSelectingText) return true;
// Do not start scrolling until last fling has been taken care of:
if (!mScroller.isFinished()) return true;
final boolean mouseTrackingAtStartOfFling = mEmulator.isMouseTrackingActive();
float SCALE = 0.25f;
if (mouseTrackingAtStartOfFling) {
mScroller.fling(0, 0, 0, -(int) (velocityY * SCALE), 0, 0, -mEmulator.mRows / 2, mEmulator.mRows / 2);
} else {
mScroller.fling(0, mTopRow, 0, -(int) (velocityY * SCALE), 0, 0, -mEmulator.getScreen().getActiveTranscriptRows(), 0);
}
post(new Runnable() {
private int mLastY = 0;
@Override
public void run() {
if (mouseTrackingAtStartOfFling != mEmulator.isMouseTrackingActive()) {
mScroller.abortAnimation();
return;
}
if (mScroller.isFinished()) return;
boolean more = mScroller.computeScrollOffset();
int newY = mScroller.getCurrY();
int diff = mouseTrackingAtStartOfFling ? (newY - mLastY) : (newY - mTopRow);
doScroll(e2, diff);
mLastY = newY;
if (more) post(this);
}
});
return true;
}
@Override
public boolean onDown(float x, float y) {
return false;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
// Do not treat is as a single confirmed tap - it may be followed by zoom.
return false;
}
@Override
public void onLongPress(MotionEvent e) {
if (!mGestureRecognizer.isInProgress() && !mIsSelectingText) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
toggleSelectingText(e);
}
}
});
mScroller = new Scroller(context);
}
/**
* @param onKeyListener
* Listener for all kinds of key events, both hardware and IME (which makes it different from that
* available with {@link View#setOnKeyListener(OnKeyListener)}.
*/
public void setOnKeyListener(TerminalKeyListener onKeyListener) {
this.mOnKeyListener = onKeyListener;
}
/**
* Attach a {@link TerminalSession} to this view.
*
* @param session
* The {@link TerminalSession} this view will be displaying.
*/
public boolean attachSession(TerminalSession session) {
if (session == mTermSession) return false;
mTopRow = 0;
mTermSession = session;
mEmulator = null;
mCombiningAccent = 0;
updateSize();
// Wait with enabling the scrollbar until we have a terminal to get scroll position from.
setVerticalScrollBarEnabled(true);
return true;
}
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
// Make the IME run in a limited "generate key events" mode.
//
// If using just "TYPE_NULL", there is a problem with the "Google Pinyin Input" being in
// word mode when used with the "En" tab available when the "Show English keyboard" option
// is enabled - see https://github.com/termux/termux-packages/issues/25.
//
// Adding TYPE_TEXT_FLAG_NO_SUGGESTIONS fixes Pinyin Input, put causes Swype to be put in
// word mode... Using TYPE_TEXT_VARIATION_VISIBLE_PASSWORD fixes that.
//
// So a bit messy. If this gets too messy it's perhaps best resolved by reverting back to just
// "TYPE_NULL" and let the Pinyin Input english keyboard be in word mode.
outAttrs.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
// Let part of the application show behind when in landscape:
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_FULLSCREEN;
return new BaseInputConnection(this, true) {
@Override
public boolean beginBatchEdit() {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: beginBatchEdit()");
return true;
}
@Override
public boolean clearMetaKeyStates(int states) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: clearMetaKeyStates(" + states + ")");
return true;
}
@Override
public boolean endBatchEdit() {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: endBatchEdit()");
return false;
}
@Override
public boolean finishComposingText() {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: finishComposingText()");
return true;
}
@Override
public int getCursorCapsMode(int reqModes) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: getCursorCapsMode(" + reqModes + ")");
int mode = 0;
if ((reqModes & TextUtils.CAP_MODE_CHARACTERS) != 0) {
mode |= TextUtils.CAP_MODE_CHARACTERS;
}
return mode;
}
@Override
public CharSequence getTextAfterCursor(int n, int flags) {
return "";
}
@Override
public CharSequence getTextBeforeCursor(int n, int flags) {
return "";
}
@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: commitText(\"" + text + "\", " + newCursorPosition + ")");
if (mEmulator == null) return true;
final int textLengthInChars = text.length();
for (int i = 0; i < textLengthInChars; i++) {
char firstChar = text.charAt(i);
int codePoint;
if (Character.isHighSurrogate(firstChar)) {
if (++i < textLengthInChars) {
codePoint = Character.toCodePoint(firstChar, text.charAt(i));
} else {
// At end of string, with no low surrogate following the high:
codePoint = TerminalEmulator.UNICODE_REPLACEMENT_CHAR;
}
} else {
codePoint = firstChar;
}
inputCodePoint(codePoint, false, false);
}
return true;
}
@Override
public boolean deleteSurroundingText(int leftLength, int rightLength) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: deleteSurroundingText(" + leftLength + ", " + rightLength + ")");
// Swype keyboard sometimes(?) sends this on backspace:
if (leftLength == 0 && rightLength == 0) leftLength = 1;
for (int i = 0; i < leftLength; i++)
sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
return true;
}
};
}
@Override
protected int computeVerticalScrollRange() {
return mEmulator == null ? 1 : mEmulator.getScreen().getActiveRows();
}
@Override
protected int computeVerticalScrollExtent() {
return mEmulator == null ? 1 : mEmulator.mRows;
}
@Override
protected int computeVerticalScrollOffset() {
return mEmulator == null ? 1 : mEmulator.getScreen().getActiveRows() + mTopRow - mEmulator.mRows;
}
public void onScreenUpdated() {
if (mEmulator == null) return;
boolean skipScrolling = false;
if (mIsSelectingText) {
// Do not scroll when selecting text.
int rowsInHistory = mEmulator.getScreen().getActiveTranscriptRows();
int rowShift = mEmulator.getScrollCounter();
if (-mTopRow + rowShift > rowsInHistory) {
// .. unless we're hitting the end of history transcript, in which
// case we abort text selection and scroll to end.
toggleSelectingText(null);
} else {
skipScrolling = true;
mTopRow -= rowShift;
mSelY1 -= rowShift;
mSelY2 -= rowShift;
}
}
if (!skipScrolling && mTopRow != 0) {
// Scroll down if not already there.
if (mTopRow < -3) {
// Awaken scroll bars only if scrolling a noticeable amount
// - we do not want visible scroll bars during normal typing
// of one row at a time.
awakenScrollBars();
}
mTopRow = 0;
}
mEmulator.clearScrollCounter();
invalidate();
}
/**
* Sets the text size, which in turn sets the number of rows and columns.
*
* @param textSize
* the new font size, in density-independent pixels.
*/
public void setTextSize(int textSize) {
mRenderer = new TerminalRenderer(textSize, mRenderer == null ? Typeface.MONOSPACE : mRenderer.mTypeface);
updateSize();
}
@Override
public boolean onCheckIsTextEditor() {
return true;
}
@Override
public boolean isOpaque() {
return true;
}
/** Send a single mouse event code to the terminal. */
void sendMouseEventCode(MotionEvent e, int button, boolean pressed) {
int x = (int) (e.getX() / mRenderer.mFontWidth) + 1;
int y = (int) ((e.getY() - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing) + 1;
if (pressed && (button == TerminalEmulator.MOUSE_WHEELDOWN_BUTTON || button == TerminalEmulator.MOUSE_WHEELUP_BUTTON)) {
if (mMouseStartDownTime == e.getDownTime()) {
x = mMouseScrollStartX;
y = mMouseScrollStartY;
} else {
mMouseStartDownTime = e.getDownTime();
mMouseScrollStartX = x;
mMouseScrollStartY = y;
}
}
mEmulator.sendMouseEvent(button, x, y, pressed);
}
/** Perform a scroll, either from dragging the screen or by scrolling a mouse wheel. */
void doScroll(MotionEvent event, int rowsDown) {
boolean up = rowsDown < 0;
int amount = Math.abs(rowsDown);
for (int i = 0; i < amount; i++) {
if (mEmulator.isMouseTrackingActive()) {
sendMouseEventCode(event, up ? TerminalEmulator.MOUSE_WHEELUP_BUTTON : TerminalEmulator.MOUSE_WHEELDOWN_BUTTON, true);
} else if (mEmulator.isAlternateBufferActive()) {
// Send up and down key events for scrolling, which is what some terminals do to make scroll work in
// e.g. less, which shifts to the alt screen without mouse handling.
handleKeyCode(up ? KeyEvent.KEYCODE_DPAD_UP : KeyEvent.KEYCODE_DPAD_DOWN, 0);
} else {
mTopRow = Math.min(0, Math.max(-(mEmulator.getScreen().getActiveTranscriptRows()), mTopRow + (up ? -1 : 1)));
if (!awakenScrollBars()) invalidate();
}
}
}
/** Overriding {@link View#onGenericMotionEvent(MotionEvent)}. */
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
if (mEmulator != null && event.isFromSource(InputDevice.SOURCE_MOUSE) && event.getAction() == MotionEvent.ACTION_SCROLL) {
// Handle mouse wheel scrolling.
boolean up = event.getAxisValue(MotionEvent.AXIS_VSCROLL) > 0.0f;
doScroll(event, up ? -3 : 3);
return true;
}
return false;
}
@SuppressLint("ClickableViewAccessibility")
@Override
@TargetApi(23)
public boolean onTouchEvent(MotionEvent ev) {
if (mEmulator == null) return true;
final int action = ev.getAction();
if (mIsSelectingText) {
int cy = (int) (ev.getY() / mRenderer.mFontLineSpacing) + mTopRow;
int cx = (int) (ev.getX() / mRenderer.mFontWidth);
switch (action) {
case MotionEvent.ACTION_UP:
mInitialTextSelection = false;
break;
case MotionEvent.ACTION_DOWN:
int distanceFromSel1 = Math.abs(cx-mSelX1) + Math.abs(cy-mSelY1);
int distanceFromSel2 = Math.abs(cx-mSelX2) + Math.abs(cy-mSelY2);
mIsDraggingLeftSelection = distanceFromSel1 <= distanceFromSel2;
mSelectionDownX = ev.getX();
mSelectionDownY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
if (mInitialTextSelection) break;
float deltaX = ev.getX() - mSelectionDownX;
float deltaY = ev.getY() - mSelectionDownY;
int deltaCols = (int) Math.ceil(deltaX / mRenderer.mFontWidth);
int deltaRows = (int) Math.ceil(deltaY / mRenderer.mFontLineSpacing);
mSelectionDownX += deltaCols * mRenderer.mFontWidth;
mSelectionDownY += deltaRows * mRenderer.mFontLineSpacing;
if (mIsDraggingLeftSelection) {
mSelX1 += deltaCols;
mSelY1 += deltaRows;
} else {
mSelX2 += deltaCols;
mSelY2 += deltaRows;
}
mSelX1 = Math.min(mEmulator.mColumns, Math.max(0, mSelX1));
mSelX2 = Math.min(mEmulator.mColumns, Math.max(0, mSelX2));
if (mSelY1 == mSelY2 && mSelX1 > mSelX2 || mSelY1 > mSelY2) {
// Switch handles.
mIsDraggingLeftSelection = !mIsDraggingLeftSelection;
int tmpX1 = mSelX1, tmpY1 = mSelY1;
mSelX1 = mSelX2; mSelY1 = mSelY2;
mSelX2 = tmpX1; mSelY2 = tmpY1;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) mActionMode.invalidateContentRect();
invalidate();
break;
default:
break;
}
mGestureRecognizer.onTouchEvent(ev);
return true;
} else if (ev.isFromSource(InputDevice.SOURCE_MOUSE)) {
if (ev.isButtonPressed(MotionEvent.BUTTON_SECONDARY)) {
if (action == MotionEvent.ACTION_DOWN) showContextMenu();
return true;
} else if (ev.isButtonPressed(MotionEvent.BUTTON_TERTIARY)) {
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboard.getPrimaryClip();
if (clipData != null) {
CharSequence paste = clipData.getItemAt(0).coerceToText(getContext());
if (!TextUtils.isEmpty(paste)) mEmulator.paste(paste.toString());
}
} else if (mEmulator.isMouseTrackingActive()) { // BUTTON_PRIMARY.
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_UP:
sendMouseEventCode(ev, TerminalEmulator.MOUSE_LEFT_BUTTON, ev.getAction() == MotionEvent.ACTION_DOWN);
break;
case MotionEvent.ACTION_MOVE:
sendMouseEventCode(ev, TerminalEmulator.MOUSE_LEFT_BUTTON_MOVED, true);
break;
}
return true;
}
}
mGestureRecognizer.onTouchEvent(ev);
return true;
}
@Override
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")");
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (mIsSelectingText) {
toggleSelectingText(null);
return true;
} else if (mOnKeyListener.shouldBackButtonBeMappedToEscape()) {
// Intercept back button to treat it as escape:
switch (event.getAction()) {
case KeyEvent.ACTION_DOWN:
return onKeyDown(keyCode, event);
case KeyEvent.ACTION_UP:
return onKeyUp(keyCode, event);
}
}
}
return super.onKeyPreIme(keyCode, event);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "onKeyDown(keyCode=" + keyCode + ", isSystem()=" + event.isSystem() + ", event=" + event + ")");
if (mEmulator == null) return true;
int metaState = event.getMetaState();
boolean controlDownFromEvent = event.isCtrlPressed();
boolean leftAltDownFromEvent = (metaState & KeyEvent.META_ALT_LEFT_ON) != 0;
boolean rightAltDownFromEvent = (metaState & KeyEvent.META_ALT_RIGHT_ON) != 0;
if (handleVirtualKeys(keyCode, event, true)) {
invalidate();
return true;
} else if (event.isSystem() && (!mOnKeyListener.shouldBackButtonBeMappedToEscape() || keyCode != KeyEvent.KEYCODE_BACK)) {
return super.onKeyDown(keyCode, event);
}
int keyMod = 0;
if (controlDownFromEvent) keyMod |= KeyHandler.KEYMOD_CTRL;
if (event.isAltPressed()) keyMod |= KeyHandler.KEYMOD_ALT;
if (event.isShiftPressed()) keyMod |= KeyHandler.KEYMOD_SHIFT;
if (handleKeyCode(keyCode, keyMod)) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "handleKeyCode() took key event");
return true;
}
// Clear Ctrl since we handle that ourselves:
int bitsToClear = KeyEvent.META_CTRL_MASK;
if (rightAltDownFromEvent) {
// Let right Alt/Alt Gr be used to compose characters.
} else {
// Use left alt to send to terminal (e.g. Left Alt+B to jump back a word), so remove:
bitsToClear |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON;
}
int effectiveMetaState = event.getMetaState() & ~bitsToClear;
int result = event.getUnicodeChar(effectiveMetaState);
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "KeyEvent#getUnicodeChar(" + effectiveMetaState + ") returned: " + result);
if (result == 0) {
return true;
}
int oldCombiningAccent = mCombiningAccent;
if ((result & KeyCharacterMap.COMBINING_ACCENT) != 0) {
// If entered combining accent previously, write it out:
if (mCombiningAccent != 0) inputCodePoint(mCombiningAccent, controlDownFromEvent, leftAltDownFromEvent);
mCombiningAccent = result & KeyCharacterMap.COMBINING_ACCENT_MASK;
} else {
if (mCombiningAccent != 0) {
int combinedChar = KeyCharacterMap.getDeadChar(mCombiningAccent, result);
if (combinedChar > 0) result = combinedChar;
mCombiningAccent = 0;
}
inputCodePoint(result, controlDownFromEvent, leftAltDownFromEvent);
}
if (mCombiningAccent != oldCombiningAccent) invalidate();
return true;
}
void inputCodePoint(int codePoint, boolean controlDownFromEvent, boolean leftAltDownFromEvent) {
if (LOG_KEY_EVENTS) {
Log.i(EmulatorDebug.LOG_TAG, "inputCodePoint(codePoint=" + codePoint + ", controlDownFromEvent=" + controlDownFromEvent + ", leftAltDownFromEvent="
+ leftAltDownFromEvent + ")");
}
int resultingKeyCode = -1; // Set if virtual key causes this to be translated to key event.
if (controlDownFromEvent || mVirtualControlKeyDown) {
if (codePoint >= 'a' && codePoint <= 'z') {
codePoint = codePoint - 'a' + 1;
} else if (codePoint >= 'A' && codePoint <= 'Z') {
codePoint = codePoint - 'A' + 1;
} else if (codePoint == ' ' || codePoint == '2') {
codePoint = 0;
} else if (codePoint == '[' || codePoint == '3') {
codePoint = 27; // ^[ (Esc)
} else if (codePoint == '\\' || codePoint == '4') {
codePoint = 28;
} else if (codePoint == ']' || codePoint == '5') {
codePoint = 29;
} else if (codePoint == '^' || codePoint == '6') {
codePoint = 30; // control-^
} else if (codePoint == '_' || codePoint == '7') {
codePoint = 31;
} else if (codePoint == '8') {
codePoint = 127; // DEL
} else if (codePoint == '9') {
resultingKeyCode = KeyEvent.KEYCODE_F11;
} else if (codePoint == '0') {
resultingKeyCode = KeyEvent.KEYCODE_F12;
}
} else if (mVirtualFnKeyDown) {
if (codePoint == 'w' || codePoint == 'W') {
resultingKeyCode = KeyEvent.KEYCODE_DPAD_UP;
} else if (codePoint == 'a' || codePoint == 'A') {
resultingKeyCode = KeyEvent.KEYCODE_DPAD_LEFT;
} else if (codePoint == 's' || codePoint == 'S') {
resultingKeyCode = KeyEvent.KEYCODE_DPAD_DOWN;
} else if (codePoint == 'd' || codePoint == 'D') {
resultingKeyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
} else if (codePoint == 'p' || codePoint == 'P') {
resultingKeyCode = KeyEvent.KEYCODE_PAGE_UP;
} else if (codePoint == 'n' || codePoint == 'N') {
resultingKeyCode = KeyEvent.KEYCODE_PAGE_DOWN;
} else if (codePoint == 't' || codePoint == 'T') {
resultingKeyCode = KeyEvent.KEYCODE_TAB;
} else if (codePoint == 'l' || codePoint == 'L') {
codePoint = '|';
} else if (codePoint == 'u' || codePoint == 'U') {
codePoint = '_';
} else if (codePoint == 'e' || codePoint == 'E') {
codePoint = 27; // ^[ (Esc)
} else if (codePoint == '.') {
codePoint = 28; // ^\
} else if (codePoint > '0' && codePoint <= '9') {
// F1-F9
resultingKeyCode = (codePoint - '1') + KeyEvent.KEYCODE_F1;
} else if (codePoint == '0') {
resultingKeyCode = KeyEvent.KEYCODE_F10;
} else if (codePoint == 'i' || codePoint == 'I') {
resultingKeyCode = KeyEvent.KEYCODE_INSERT;
} else if (codePoint == 'x' || codePoint == 'X') {
resultingKeyCode = KeyEvent.KEYCODE_FORWARD_DEL;
} else if (codePoint == 'h' || codePoint == 'H') {
resultingKeyCode = KeyEvent.KEYCODE_MOVE_HOME;
} else if (codePoint == 'f' || codePoint == 'F') {
// As left alt+f, jumping forward in readline:
codePoint = 'f';
leftAltDownFromEvent = true;
} else if (codePoint == 'b' || codePoint == 'B') {
// As left alt+b, jumping forward in readline:
codePoint = 'b';
leftAltDownFromEvent = true;
} else if (codePoint == 'v' || codePoint == 'V') {
codePoint = -1;
AudioManager audio = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
audio.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME, AudioManager.USE_DEFAULT_STREAM_TYPE, AudioManager.FLAG_SHOW_UI);
}
}
if (codePoint > -1) {
if (resultingKeyCode > -1) {
handleKeyCode(resultingKeyCode, 0);
} else {
// Work around bluetooth keyboards sending funny unicode characters instead
// of the more normal ones from ASCII that terminal programs expect - the
// desire to input the original characters should be low.
switch (codePoint) {
case 0x02DC: // SMALL TILDE.
codePoint = 0x007E; // TILDE (~).
break;
case 0x02CB: // MODIFIER LETTER GRAVE ACCENT.
codePoint = 0x0060; // GRAVE ACCENT (`).
break;
case 0x02C6: // MODIFIER LETTER CIRCUMFLEX ACCENT.
codePoint = 0x005E; // CIRCUMFLEX ACCENT (^).
break;
}
// If left alt, send escape before the code point to make e.g. Alt+B and Alt+F work in readline:
mTermSession.writeCodePoint(leftAltDownFromEvent, codePoint);
}
}
}
/** Input the specified keyCode if applicable and return if the input was consumed. */
public boolean handleKeyCode(int keyCode, int keyMod) {
TerminalEmulator term = mTermSession.getEmulator();
String code = KeyHandler.getCode(keyCode, keyMod, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode());
if (code == null) return false;
mTermSession.write(code);
return true;
}
/**
* Called when a key is released in the view.
*
* @param keyCode
* The keycode of the key which was released.
* @param event
* A {@link KeyEvent} describing the event.
* @return Whether the event was handled.
*/
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "onKeyUp(keyCode=" + keyCode + ", event=" + event + ")");
if (mEmulator == null) return true;
if (handleVirtualKeys(keyCode, event, false)) {
invalidate();
return true;
} else if (event.isSystem()) {
// Let system key events through.
return super.onKeyUp(keyCode, event);
}
return true;
}
/** 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) {
// Do not steal dedicated buttons from a full external keyboard.
return false;
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "handleVirtualKeys(down=" + down + ") taking ctrl event");
mVirtualControlKeyDown = down;
return true;
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "handleVirtualKeys(down=" + down + ") taking Fn event");
mVirtualFnKeyDown = down;
return true;
}
return false;
}
public void checkForFontAndColors() {
try {
// Hard-coded paths since this file is used also in Termux:Float.
@SuppressLint("SdCardPath") File fontFile = new File("/data/data/com.termux/files/home/.termux/font.ttf");
@SuppressLint("SdCardPath") File colorsFile = new File("/data/data/com.termux/files/home/.termux/colors.properties");
final Properties props = new Properties();
if (colorsFile.isFile()) {
try (InputStream in = new FileInputStream(colorsFile)) {
props.load(in);
}
}
TerminalColors.COLOR_SCHEME.updateWith(props);
if (mEmulator != null) mEmulator.mColors.reset();
final Typeface newTypeface = fontFile.exists() ? Typeface.createFromFile(fontFile) : Typeface.MONOSPACE;
mRenderer = new TerminalRenderer(mRenderer.mTextSize, newTypeface);
updateSize();
invalidate();
} catch (Exception e) {
Log.e(EmulatorDebug.LOG_TAG, "Error in checkForFontAndColors()", e);
}
}
/**
* This is called during layout when the size of this view has changed. If you were just added to the view
* hierarchy, you're called with the old values of 0.
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
updateSize();
}
/** Check if the terminal size in rows and columns should be updated. */
public void updateSize() {
int viewWidth = getWidth();
int viewHeight = getHeight();
if (viewWidth == 0 || viewHeight == 0 || mTermSession == null) return;
// Set to 80 and 24 if you want to enable vttest.
int newColumns = Math.max(8, (int) (viewWidth / mRenderer.mFontWidth));
int newRows = Math.max(8, (viewHeight - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing);
if (mEmulator == null || (newColumns != mEmulator.mColumns || newRows != mEmulator.mRows)) {
mTermSession.updateSize(newColumns, newRows);
mEmulator = mTermSession.getEmulator();
mTopRow = 0;
scrollTo(0, 0);
invalidate();
}
}
@Override
protected void onDraw(Canvas canvas) {
if (mEmulator == null) {
canvas.drawColor(0XFF000000);
} else {
mRenderer.render(mEmulator, canvas, mTopRow, mSelY1, mSelY2, mSelX1, mSelX2);
if (mIsSelectingText) {
final int gripHandleWidth = mLeftSelectionHandle.getIntrinsicWidth();
final int gripHandleMargin = gripHandleWidth / 4; // See the png.
int right = Math.round((mSelX1) * mRenderer.mFontWidth) + gripHandleMargin;
int top = (mSelY1+1 - mTopRow)*mRenderer.mFontLineSpacing + mRenderer.mFontLineSpacingAndAscent;
mLeftSelectionHandle.setBounds(right - gripHandleWidth, top, right, top + mLeftSelectionHandle.getIntrinsicHeight());
mLeftSelectionHandle.draw(canvas);
int left = Math.round((mSelX2+1)*mRenderer.mFontWidth) - gripHandleMargin;
top = (mSelY2+1 - mTopRow) *mRenderer.mFontLineSpacing + mRenderer.mFontLineSpacingAndAscent;
mRightSelectionHandle.setBounds(left, top, left + gripHandleWidth, top + mRightSelectionHandle.getIntrinsicHeight());
mRightSelectionHandle.draw(canvas);
}
}
}
/** Toggle text selection mode in the view. */
@TargetApi(23)
public void toggleSelectingText(MotionEvent ev) {
mIsSelectingText = !mIsSelectingText;
mOnKeyListener.copyModeChanged(mIsSelectingText);
if (mIsSelectingText) {
if (mLeftSelectionHandle == null) {
mLeftSelectionHandle = (BitmapDrawable) getContext().getDrawable(R.drawable.text_select_handle_left_material);
mRightSelectionHandle = (BitmapDrawable) getContext().getDrawable(R.drawable.text_select_handle_right_material);
}
int cx = (int) (ev.getX() / mRenderer.mFontWidth);
final boolean eventFromMouse = ev.isFromSource(InputDevice.SOURCE_MOUSE);
// Offset for finger:
final int SELECT_TEXT_OFFSET_Y = eventFromMouse ? 0 : -40;
int cy = (int) ((ev.getY() + SELECT_TEXT_OFFSET_Y) / mRenderer.mFontLineSpacing) + mTopRow;
mSelX1 = mSelX2 = cx;
mSelY1 = mSelY2 = cy;
TerminalBuffer screen = mEmulator.getScreen();
if (!" ".equals(screen.getSelectedText(mSelX1, mSelY1, mSelX1, mSelY1))) {
// Selecting something other than whitespace. Expand to word.
while (mSelX1 > 0 && !"".equals(screen.getSelectedText(mSelX1-1, mSelY1, mSelX1-1, mSelY1))) {
mSelX1--;
}
while (mSelX2 < mEmulator.mColumns-1 && !"".equals(screen.getSelectedText(mSelX2+1, mSelY1, mSelX2+1, mSelY1))) {
mSelX2++;
}
}
mInitialTextSelection = true;
mIsDraggingLeftSelection = true;
mSelectionDownX = ev.getX();
mSelectionDownY = ev.getY();
final ActionMode.Callback callback = new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
final int[] ACTION_MODE_ATTRS = { android.R.attr.actionModeCopyDrawable, android.R.attr.actionModePasteDrawable, };
TypedArray styledAttributes = getContext().obtainStyledAttributes(ACTION_MODE_ATTRS);
try {
int show = MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT;
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
menu.add(Menu.NONE, 1, Menu.NONE, R.string.copy_text).setIcon(styledAttributes.getResourceId(0, 0)).setShowAsAction(show);
menu.add(Menu.NONE, 2, Menu.NONE, R.string.paste_text).setIcon(styledAttributes.getResourceId(1, 0)).setEnabled(clipboard.hasPrimaryClip()).setShowAsAction(show);
menu.add(Menu.NONE, 3, Menu.NONE, R.string.text_selection_more);
} finally {
styledAttributes.recycle();
}
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case 1:
String selectedText = mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2).trim();
mTermSession.clipboardText(selectedText);
break;
case 2:
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboard.getPrimaryClip();
if (clipData != null) {
CharSequence paste = clipData.getItemAt(0).coerceToText(getContext());
if (!TextUtils.isEmpty(paste)) mEmulator.paste(paste.toString());
}
break;
case 3:
showContextMenu();
break;
}
toggleSelectingText(null);
return true;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
}
};
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mActionMode = startActionMode(new ActionMode.Callback2() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
return callback.onCreateActionMode(mode, menu);
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
return callback.onActionItemClicked(mode, item);
}
@Override
public void onDestroyActionMode(ActionMode mode) {
// Ignore.
}
@Override
public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
int x1 = Math.round(mSelX1 * mRenderer.mFontWidth);
int x2 = Math.round(mSelX2 * mRenderer.mFontWidth);
int y1 = Math.round((mSelY1 - mTopRow) * mRenderer.mFontLineSpacing);
int y2 = Math.round((mSelY2 + 1 - mTopRow) * mRenderer.mFontLineSpacing);
outRect.set(Math.min(x1, x2), y1, Math.max(x1, x2), y2);
}
}, ActionMode.TYPE_FLOATING);
} else {
mActionMode = startActionMode(callback);
}
invalidate();
} else {
mActionMode.finish();
mSelX1 = mSelY1 = mSelX2 = mSelY2 = -1;
invalidate();
}
}
public TerminalSession getCurrentSession() {
return mTermSession;
}
}

View File

@@ -1,5 +0,0 @@
APP_ABI := armeabi-v7a x86
APP_PLATFORM := android-21
NDK_TOOLCHAIN_VERSION := 4.9
APP_CFLAGS := -std=c11 -Wall -Wextra -Os -fno-stack-protector
APP_LDFLAGS = -nostdlib -Wl,--gc-sections

View File

@@ -1,219 +0,0 @@
#include <dirent.h>
#include <fcntl.h>
#include <jni.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <termios.h>
#include <unistd.h>
#define TERMUX_UNUSED(x) x __attribute__((__unused__))
#ifdef __APPLE__
# define LACKS_PTSNAME_R
#endif
static int throw_runtime_exception(JNIEnv* env, char const* message)
{
jclass exClass = (*env)->FindClass(env, "java/lang/RuntimeException");
(*env)->ThrowNew(env, exClass, message);
return -1;
}
static int create_subprocess(JNIEnv* env,
char const* cmd,
char const* cwd,
char* const argv[],
char** envp,
int* pProcessId,
jint rows,
jint columns)
{
int ptm = open("/dev/ptmx", O_RDWR | O_CLOEXEC);
if (ptm < 0) return throw_runtime_exception(env, "Cannot open /dev/ptmx");
#ifdef LACKS_PTSNAME_R
char* devname;
#else
char devname[64];
#endif
if (grantpt(ptm) || unlockpt(ptm) ||
#ifdef LACKS_PTSNAME_R
(devname = ptsname(ptm)) == NULL
#else
ptsname_r(ptm, devname, sizeof(devname))
#endif
) {
return throw_runtime_exception(env, "Cannot grantpt()/unlockpt()/ptsname_r() on /dev/ptmx");
}
// Enable UTF-8 mode and disable flow control to prevent Ctrl+S from locking up the display.
struct termios tios;
tcgetattr(ptm, &tios);
tios.c_iflag |= IUTF8;
tios.c_iflag &= ~(IXON | IXOFF);
tcsetattr(ptm, TCSANOW, &tios);
/** Set initial winsize. */
struct winsize sz = { .ws_row = rows, .ws_col = columns };
ioctl(ptm, TIOCSWINSZ, &sz);
pid_t pid = fork();
if (pid < 0) {
return throw_runtime_exception(env, "Fork failed");
} else if (pid > 0) {
*pProcessId = (int) pid;
return ptm;
} else {
// Clear signals which the Android java process may have blocked:
sigset_t signals_to_unblock;
sigfillset(&signals_to_unblock);
sigprocmask(SIG_UNBLOCK, &signals_to_unblock, 0);
close(ptm);
setsid();
int pts = open(devname, O_RDWR);
if (pts < 0) exit(-1);
dup2(pts, 0);
dup2(pts, 1);
dup2(pts, 2);
DIR* self_dir = opendir("/proc/self/fd");
if (self_dir != NULL) {
int self_dir_fd = dirfd(self_dir);
struct dirent* entry;
while ((entry = readdir(self_dir)) != NULL) {
int fd = atoi(entry->d_name);
if(fd > 2 && fd != self_dir_fd) close(fd);
}
closedir(self_dir);
}
clearenv();
if (envp) for (; *envp; ++envp) putenv(*envp);
if (chdir(cwd) != 0) {
char* error_message;
// No need to free asprintf()-allocated memory since doing execvp() or exit() below.
if (asprintf(&error_message, "chdir(\"%s\")", cwd) == -1) error_message = "chdir()";
perror(error_message);
fflush(stderr);
}
execvp(cmd, argv);
// Show terminal output about failing exec() call:
char* error_message;
if (asprintf(&error_message, "exec(\"%s\")", cmd) == -1) error_message = "exec()";
perror(error_message);
_exit(1);
}
}
JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_createSubprocess(
JNIEnv* env,
jclass TERMUX_UNUSED(clazz),
jstring cmd,
jstring cwd,
jobjectArray args,
jobjectArray envVars,
jintArray processIdArray,
jint rows,
jint columns)
{
jsize size = args ? (*env)->GetArrayLength(env, args) : 0;
char** argv = NULL;
if (size > 0) {
argv = (char**) malloc((size + 1) * sizeof(char*));
if (!argv) return throw_runtime_exception(env, "Couldn't allocate argv array");
for (int i = 0; i < size; ++i) {
jstring arg_java_string = (jstring) (*env)->GetObjectArrayElement(env, args, i);
char const* arg_utf8 = (*env)->GetStringUTFChars(env, arg_java_string, NULL);
if (!arg_utf8) return throw_runtime_exception(env, "GetStringUTFChars() failed for argv");
argv[i] = strdup(arg_utf8);
(*env)->ReleaseStringUTFChars(env, arg_java_string, arg_utf8);
}
argv[size] = NULL;
}
size = envVars ? (*env)->GetArrayLength(env, envVars) : 0;
char** envp = NULL;
if (size > 0) {
envp = (char**) malloc((size + 1) * sizeof(char *));
if (!envp) return throw_runtime_exception(env, "malloc() for envp array failed");
for (int i = 0; i < size; ++i) {
jstring env_java_string = (jstring) (*env)->GetObjectArrayElement(env, envVars, i);
char const* env_utf8 = (*env)->GetStringUTFChars(env, env_java_string, 0);
if (!env_utf8) return throw_runtime_exception(env, "GetStringUTFChars() failed for env");
envp[i] = strdup(env_utf8);
(*env)->ReleaseStringUTFChars(env, env_java_string, env_utf8);
}
envp[size] = NULL;
}
int procId = 0;
char const* cmd_cwd = (*env)->GetStringUTFChars(env, cwd, NULL);
char const* cmd_utf8 = (*env)->GetStringUTFChars(env, cmd, NULL);
int ptm = create_subprocess(env, cmd_utf8, cmd_cwd, argv, envp, &procId, rows, columns);
(*env)->ReleaseStringUTFChars(env, cmd, cmd_utf8);
(*env)->ReleaseStringUTFChars(env, cmd, cmd_cwd);
if (argv) {
for (char** tmp = argv; *tmp; ++tmp) free(*tmp);
free(argv);
}
if (envp) {
for (char** tmp = envp; *tmp; ++tmp) free(*tmp);
free(envp);
}
int* pProcId = (int*) (*env)->GetPrimitiveArrayCritical(env, processIdArray, NULL);
if (!pProcId) return throw_runtime_exception(env, "JNI call GetPrimitiveArrayCritical(processIdArray, &isCopy) failed");
*pProcId = procId;
(*env)->ReleasePrimitiveArrayCritical(env, processIdArray, pProcId, 0);
return ptm;
}
JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_setPtyWindowSize(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fd, jint rows, jint cols)
{
struct winsize sz = { .ws_row = rows, .ws_col = cols };
ioctl(fd, TIOCSWINSZ, &sz);
}
JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_setPtyUTF8Mode(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fd)
{
struct termios tios;
tcgetattr(fd, &tios);
if ((tios.c_iflag & IUTF8) == 0) {
tios.c_iflag |= IUTF8;
tcsetattr(fd, TCSANOW, &tios);
}
}
JNIEXPORT int JNICALL Java_com_termux_terminal_JNI_waitFor(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint pid)
{
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
return WEXITSTATUS(status);
} else if (WIFSIGNALED(status)) {
return -WTERMSIG(status);
} else {
// Should never happen - waitpid(2) says "One of the first three macros will evaluate to a non-zero (true) value".
return 0;
}
}
JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_hangupProcessGroup(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint procId)
{
killpg(procId, SIGHUP);
}
JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_close(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fileDescriptor)
{
close(fileDescriptor);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 786 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 983 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFF"
android:pathData="M 12, 12
m -10.5, 0
a 10.5,10.5 0 1,0 21,0
a 10.5,10.5 0 1,0 -21,0"/>
<path
android:fillColor="#FF000000"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13h-4v4h-2v-4L7,13v-2h4L11,7h2v4h4v2z"/>
</vector>

View File

@@ -0,0 +1,33 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="48dp"
android:width="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<!--
https://material.google.com/style/icons.html
-->
<!-- Screen border. -->
<path android:fillColor="#00000000"
android:strokeColor="#000"
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"
/>
<!-- Block cursor. -->
<path android:fillColor="#000"
android:pathData="M14,14
l5,0
l0,10
l-5,0"
/>
</vector>

View File

@@ -1,58 +1,75 @@
<com.termux.drawer.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/drawer_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" > android:layout_height="match_parent"
android:orientation="vertical">
<com.termux.view.TerminalView <android.support.v4.widget.DrawerLayout
android:id="@+id/terminal_view" android:id="@+id/drawer_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_alignParentTop="true"
android:focusableInTouchMode="true" android:layout_above="@+id/viewpager"
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape" android:layout_height="match_parent">
android:scrollbars="vertical" />
<LinearLayout <com.termux.view.TerminalView
android:id="@+id/left_drawer" android:id="@+id/terminal_view"
android:layout_width="240dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="@android:color/white"
android:choiceMode="singleChoice"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"
android:orientation="vertical" >
<ListView
android:id="@+id/left_drawer_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="match_parent"
android:layout_gravity="top" android:focusableInTouchMode="true"
android:layout_weight="1" android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
android:choiceMode="singleChoice" android:scrollbars="vertical" />
android:longClickable="true" />
<LinearLayout <LinearLayout
style="?android:attr/buttonBarStyle" android:id="@+id/left_drawer"
android:layout_width="match_parent" android:layout_width="240dp"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:orientation="horizontal" > android:layout_gravity="start"
android:background="@android:color/white"
android:choiceMode="singleChoice"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"
android:descendantFocusability="blocksDescendants"
android:orientation="vertical">
<Button <ListView
android:id="@+id/toggle_keyboard_button" android:id="@+id/left_drawer_list"
style="?android:attr/buttonBarButtonStyle" android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_gravity="top"
android:layout_weight="1"
android:choiceMode="singleChoice"
android:longClickable="true" />
<LinearLayout
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:orientation="horizontal">
android:text="@string/toggle_soft_keyboard" />
<Button <Button
android:id="@+id/new_session_button" android:id="@+id/toggle_keyboard_button"
style="?android:attr/buttonBarButtonStyle" style="?android:attr/buttonBarButtonStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:text="@string/new_session" /> android:text="@string/toggle_soft_keyboard" />
<Button
android:id="@+id/new_session_button"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/new_session" />
</LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout>
</com.termux.drawer.DrawerLayout> </android.support.v4.widget.DrawerLayout>
<android.support.v4.view.ViewPager
android:id="@+id/viewpager"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@android:drawable/screen_background_dark_transparent"
android:layout_alignParentBottom="true" />
</RelativeLayout>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<com.termux.app.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/extra_keys"
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentBottom="true"
android:orientation="horizontal" />

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<EditText xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/text_input"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:imeOptions="actionSend|flagNoFullscreen"
android:maxLines="1"
android:inputType="text"
android:textColor="@android:color/white"
android:paddingTop="0dp"
android:textCursorDrawable="@null"
android:paddingBottom="0dp"
tools:ignore="LabelFor" />

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="8dp"
android:paddingRight="8dp">
<ListView android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:drawSelectorOnTop="false"/>
<TextView android:id="@android:id/empty"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/empty_folder"/>
</LinearLayout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 691 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -11,10 +11,6 @@
<string name="share_transcript_title">Terminal transcript</string> <string name="share_transcript_title">Terminal transcript</string>
<string name="help">Help</string> <string name="help">Help</string>
<string name="welcome_dialog_title">Welcome to Termux</string>
<string name="welcome_dialog_body">Long press and select <i>More…</i> to show a menu where <i>Help</i> is available.\n\nExecute <b>apt update</b> to update the packages list before installing packages.</string>
<string name="welcome_dialog_dont_show_again_button">Do not show again</string>
<string name="bootstrap_installer_body">Installing…</string> <string name="bootstrap_installer_body">Installing…</string>
<string name="bootstrap_error_title">Unable to install</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.\n\nCheck your network connection and try again.</string>
@@ -34,12 +30,8 @@
<string name="select_url_copied_to_clipboard">URL copied to clipboard</string> <string name="select_url_copied_to_clipboard">URL copied to clipboard</string>
<string name="share_transcript_chooser_title">Send text to:</string> <string name="share_transcript_chooser_title">Send text to:</string>
<string name="paste_text">Paste</string> <string name="kill_process">Kill process (%d)</string>
<string name="copy_text">Copy</string> <string name="confirm_kill_process">Really kill this session?</string>
<string name="text_selection_more">More…</string>
<string name="kill_process">Hangup</string>
<string name="confirm_kill_process">Close this process?</string>
<string name="session_rename_title">Set session name</string> <string name="session_rename_title">Set session name</string>
<string name="session_rename_positive_button">Set</string> <string name="session_rename_positive_button">Set</string>
@@ -50,10 +42,8 @@
<string name="styling_install">Install</string> <string name="styling_install">Install</string>
<string name="notification_action_exit">Exit</string> <string name="notification_action_exit">Exit</string>
<string name="notification_action_wakelock">Wake</string> <string name="notification_action_wake_lock">Acquire wakelock</string>
<string name="notification_action_wifilock">Wifi</string> <string name="notification_action_wake_unlock">Release wakelock</string>
<string name="empty_folder">Empty folder.</string>
<string name="file_received_title">Save file in ~/downloads/</string> <string name="file_received_title">Save file in ~/downloads/</string>
<string name="file_received_edit_button">Edit</string> <string name="file_received_edit_button">Edit</string>

View File

@@ -5,6 +5,7 @@
<!-- NOTE: Cannot use "Light." since it hides the terminal scrollbar on the default black background. --> <!-- 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"> <style name="Theme.Termux" parent="@android:style/Theme.Material.Light.NoActionBar">
<item name="android:statusBarColor">#000000</item> <item name="android:statusBarColor">#000000</item>
<item name="android:colorPrimary">#FF000000</item>
<item name="android:windowBackground">@android:color/black</item> <item name="android:windowBackground">@android:color/black</item>
<!-- Seen in buttons on left drawer: --> <!-- Seen in buttons on left drawer: -->
@@ -13,6 +14,10 @@
<!-- Avoid action mode toolbar pushing down terminal content when <!-- Avoid action mode toolbar pushing down terminal content when
selecting text on pre-6.0 (non-floating toolbar). --> selecting text on pre-6.0 (non-floating toolbar). -->
<item name="android:windowActionModeOverlay">true</item> <item name="android:windowActionModeOverlay">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> </style>
<style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert"> <style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">

View File

@@ -0,0 +1,14 @@
<shortcuts xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:shortcutId="new_session"
android:enabled="true"
android:icon="@drawable/ic_new_session"
android:shortcutShortLabel="@string/new_session"
tools:targetApi="n_mr1">
<intent
android:action="android.intent.action.RUN"
android:targetPackage="com.termux"
android:targetClass="com.termux.app.TermuxActivity"/>
</shortcut>
</shortcuts>

20
art/copy-to-other-apps.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/sh
for DENSITY in mdpi hdpi xhdpi xxhdpi xxxhdpi; do
FOLDER=../app/src/main/res/mipmap-$DENSITY
for FILE in ic_launcher ic_launcher_round; do
PNG=$FOLDER/$FILE.png
# Update other apps:
for APP in api boot styling tasker widget; do
APPDIR=../../termux-$APP
if [ -d $APPDIR ]; then
APP_FOLDER=$APPDIR/app/src/main/res/mipmap-$DENSITY
mkdir -p $APP_FOLDER
cp $PNG $APP_FOLDER/$FILE.png
fi
done
done
done

30
art/feature-graphic.svg Normal file
View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<!--
This is a feature graphic:
https://support.google.com/googleplay/android-developer/answer/1078870
- 1024px by 500px, no alpha
- Don't include any copy or important visual information near the borders of the asset,
specifically near the bottom third of the frame.
- Try to center align any logo/copy information in the vertical and horizontal center of the frame.
- If adding text, use large font sizes.
- Your graphic may be displayed alone without the app icon.
-->
<svg xmlns="http://www.w3.org/2000/svg"
version="1.1"
viewBox="0 0 1024 500">
<rect fill="#0" width="100%" height="100%" />
<text id="shell_prompt"
x="130"
y="330"
style="fill: #ffffff; font-size: 124px; font-family: Menlo;">
<!--
<tspan>$</tspan>
<tspan x="290">Termux</tspan>
<tspan x="734">█</tspan>
-->
<tspan>$ Termux █</tspan>
</text>
</svg>

After

Width:  |  Height:  |  Size: 962 B

View File

@@ -0,0 +1,5 @@
#!/bin/bash
echo "Generating feature graphics to ~/termux-icons/termux-feature-graphic.png..."
mkdir -p ~/termux-icons/
rsvg-convert feature-graphic.svg > ~/termux-icons/feature-graphic.png

20
art/generate-launcher-images.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/sh
for DENSITY in mdpi hdpi xhdpi xxhdpi xxxhdpi; do
case $DENSITY in
mdpi) SIZE=48;;
hdpi) SIZE=72;;
xhdpi) SIZE=96;;
xxhdpi) SIZE=144;;
xxxhdpi) SIZE=192;;
esac
FOLDER=../app/src/main/res/mipmap-$DENSITY
mkdir -p $FOLDER
for FILE in ic_launcher ic_launcher_round; do
PNG=$FOLDER/$FILE.png
rsvg-convert -w $SIZE -h $SIZE $FILE.svg > $PNG
zopflipng -y $PNG $PNG
done
done

9
art/generate-tv-banner.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/bash
echo "Generating feature graphics to ~/termux-icons/termux-feature-graphic.png..."
mkdir -p ~/termux-icons/
# The Android TV banner on google play (1280x720) has same aspect ratio
# as the banner in the app (320x180).
rsvg-convert -w 1280 -h 720 tv-banner.svg > ~/termux-icons/tv-banner.png
rsvg-convert -w 320 -h 180 tv-banner.svg > ../app/src/main/res/drawable/banner.png

26
art/ic_launcher.svg Normal file
View File

@@ -0,0 +1,26 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<!-- Screen and border. -->
<path fill="#000"
stroke="#BFCBCD"
stroke-width="2"
d="M9,6
l30,0
q3 0,3 3
l0,30
q0 3, -3 3
l-30,0
q-3 0, -3-3
l0 -30
q0 -3, 3 -3"
/>
<!-- Block cursor. -->
<path fill="#FFF"
d="M14,14
l5,0
l0,10
l-5,0"
/>
</svg>

After

Width:  |  Height:  |  Size: 512 B

21
art/ic_launcher_round.svg Normal file
View File

@@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<!-- Screen and border. -->
<path fill="#000"
stroke="#BFCBCD"
stroke-width="2"
d="M 24, 24
m -21, 0
a 21,21 0 1,0 42,0
a 21,21 0 1,0 -42,0"
/>
<!-- Block cursor. -->
<path fill="#FFF"
d="M15,15
l5,0
l0,10
l-5,0"
/>
</svg>

After

Width:  |  Height:  |  Size: 429 B

23
art/tv-banner.svg Normal file
View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<!--
This is a tv banner graphic:
https://developer.android.com/design/tv/patterns.html#banner
- Size: 320 x 180 px, xhdpi resource in app
- Size: 1280 x 720 in google play.
- Text must be included in the image. If your app is available in more
than one language, you must provide versions of the banner image for each supported language.
-->
<svg xmlns="http://www.w3.org/2000/svg"
version="1.1"
viewBox="0 0 1280 720">
<rect fill="#0" width="100%" height="100%" />
<text id="shell_prompt"
x="200"
y="410"
style="fill: #ffffff; font-size: 210px; font-family: Menlo, Monospace;">
<tspan>Termux ▌</tspan>
</text>
</svg>

After

Width:  |  Height:  |  Size: 736 B

View File

@@ -1,21 +0,0 @@
#!/bin/sh
# Script to build jni libraries while waiting for the new gradle build system:
# http://tools.android.com/tech-docs/new-build-system/gradle-experimental#TOC-Ndk-Integration
set -e -u
SRC_JNILIBS=app/src/main/jniLibs/
rm -Rf $SRC_JNILIBS
mkdir -p $SRC_JNILIBS
PROJECTDIR=`mktemp -d`
JNIDIR=$PROJECTDIR/jni
LIBSDIR=$PROJECTDIR/libs
mkdir $JNIDIR
cp app/src/main/jni/* $JNIDIR/
ndk-build NDK_PROJECT_PATH=$PROJECTDIR
cp -Rf $LIBSDIR/* $SRC_JNILIBS
rm -Rf $PROJECTDIR

View File

@@ -5,10 +5,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:1.5.0' classpath 'com.android.tools.build:gradle:2.3.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
} }
} }

View File

@@ -1,18 +1,15 @@
# Project-wide Gradle settings. ## Project-wide Gradle settings.
#
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit # For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html # http://www.gradle.org/docs/current/userguide/build_environment.html
#
# Specifies the JVM arguments used for the daemon process. # Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings. # The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx10248m -XX:MaxPermSize=256m # Default value: -Xmx10248m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
#
# When configured, Gradle will run in incubating parallel mode. # When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit # This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true # org.gradle.parallel=true
org.gradle.jvmargs=-Xmx2048M

Binary file not shown.

View File

@@ -1,6 +1,6 @@
#Tue Mar 15 00:24:33 CET 2016 #Mon Mar 06 01:34:12 CET 2017
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-bin.zip

68
gradlew vendored
View File

@@ -1,4 +1,4 @@
#!/usr/bin/env bash #!/usr/bin/env sh
############################################################################## ##############################################################################
## ##
@@ -6,12 +6,30 @@
## ##
############################################################################## ##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Attempt to set APP_HOME
DEFAULT_JVM_OPTS="" # Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle" APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"` 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=""
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum" MAX_FD="maximum"
@@ -30,6 +48,7 @@ die ( ) {
cygwin=false cygwin=false
msys=false msys=false
darwin=false darwin=false
nonstop=false
case "`uname`" in case "`uname`" in
CYGWIN* ) CYGWIN* )
cygwin=true cygwin=true
@@ -40,26 +59,11 @@ case "`uname`" in
MINGW* ) MINGW* )
msys=true msys=true
;; ;;
NONSTOP* )
nonstop=true
;;
esac esac
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
@@ -85,7 +89,7 @@ location of your Java installation."
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n` MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
@@ -150,11 +154,19 @@ if $cygwin ; then
esac esac
fi fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules # Escape application args
function splitJvmOpts() { save ( ) {
JVM_OPTS=("$@") for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
} }
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS APP_ARGS=$(save "$@")
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" # 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" "$@"

84
gradlew.bat vendored Normal file
View File

@@ -0,0 +1,84 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@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=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,78 @@
// Start https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle
apply plugin: 'com.github.dcendents.android-maven'
group = publishedGroupId // Maven Group ID for the artifact
install {
repositories.mavenInstaller {
pom {
project {
packaging 'aar'
groupId publishedGroupId
artifactId artifact
name libraryName
description libraryDescription
url siteUrl
licenses {
license {
name 'GNU General Public License version 3'
url 'https://opensource.org/licenses/gpl-3.0.html'
}
}
developers {
developer {
id 'fornwall'
name 'Fredrik Fornwall'
email 'fredrik@fornwall.net'
}
}
scm {
connection gitUrl
developerConnection gitUrl
url siteUrl
}
}
}
}
}
// End https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle
// Start https://raw.githubusercontent.com/nuuneoi/JCenter/master/bintrayv1.gradle
apply plugin: 'com.jfrog.bintray'
version = libraryVersion
task sourcesJar(type: Jar) {
classifier = 'sources'
from android.sourceSets.main.java.srcDirs
}
artifacts {
archives sourcesJar
}
bintray {
user = System.getenv('BINTRAY_USER')
key = System.getenv('BINTRAY_API_KEY')
configurations = ['archives']
pkg {
repo = 'maven'
name = bintrayName
userOrg = 'termux'
desc = libraryDescription
websiteUrl = siteUrl
vcsUrl = gitUrl
licenses = ['GPL-3.0']
publish = true
publicDownloadNumbers = true
version {
desc = libraryDescription
gpg {
sign = false //Determines whether to GPG sign the files. The default is false
// passphrase = properties.getProperty("bintray.gpg.password")
}
}
}
}

View File

@@ -1 +1 @@
include ':app' include ':app', ':terminal-emulator', ':terminal-view'

View File

@@ -0,0 +1,62 @@
plugins {
id "com.jfrog.bintray" version "1.7"
id "com.github.dcendents.android-maven" version "1.5"
}
apply plugin: 'com.android.library'
ext {
bintrayName = 'terminal-emulator'
publishedGroupId = 'com.termux'
libraryName = 'TerminalEmulator'
artifact = 'terminal-emulator'
libraryDescription = 'The terminal emulator used in Termux'
siteUrl = 'https://github.com/termux/termux'
gitUrl = 'https://github.com/termux/termux.git'
libraryVersion = '0.49'
}
android {
compileSdkVersion 25
buildToolsVersion "25.0.3"
defaultConfig {
minSdkVersion 21
targetSdkVersion 25
externalNativeBuild {
ndkBuild {
cFlags "-std=c11", "-Wall", "-Wextra", "-Werror", "-Os", "-fno-stack-protector", "-Wl,--gc-sections"
}
}
ndk {
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
ndkBuild {
path "src/main/jni/Android.mk"
}
}
}
tasks.withType(Test) {
testLogging {
events "started", "passed", "skipped", "failed"
}
}
dependencies {
testCompile 'junit:junit:4.12'
}
apply from: '../scripts/bintray-publish.gradle'

25
terminal-emulator/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,25 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /Users/fornwall/lib/android-sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.termux.terminal">
</manifest>

View File

@@ -0,0 +1,108 @@
package com.termux.terminal;
/** A circular byte buffer allowing one producer and one consumer thread. */
final class ByteQueue {
private final byte[] mBuffer;
private int mHead;
private int mStoredBytes;
private boolean mOpen = true;
public ByteQueue(int size) {
mBuffer = new byte[size];
}
public synchronized void close() {
mOpen = false;
notify();
}
public synchronized int read(byte[] buffer, boolean block) {
while (mStoredBytes == 0 && mOpen) {
if (block) {
try {
wait();
} catch (InterruptedException e) {
// Ignore.
}
} else {
return 0;
}
}
if (!mOpen) return -1;
int totalRead = 0;
int bufferLength = mBuffer.length;
boolean wasFull = bufferLength == mStoredBytes;
int length = buffer.length;
int offset = 0;
while (length > 0 && mStoredBytes > 0) {
int oneRun = Math.min(bufferLength - mHead, mStoredBytes);
int bytesToCopy = Math.min(length, oneRun);
System.arraycopy(mBuffer, mHead, buffer, offset, bytesToCopy);
mHead += bytesToCopy;
if (mHead >= bufferLength) mHead = 0;
mStoredBytes -= bytesToCopy;
length -= bytesToCopy;
offset += bytesToCopy;
totalRead += bytesToCopy;
}
if (wasFull) notify();
return totalRead;
}
/**
* Attempt to write the specified portion of the provided buffer to the queue.
* <p/>
* Returns whether the output was totally written, false if it was closed before.
*/
public boolean write(byte[] buffer, int offset, int lengthToWrite) {
if (lengthToWrite + offset > buffer.length) {
throw new IllegalArgumentException("length + offset > buffer.length");
} else if (lengthToWrite <= 0) {
throw new IllegalArgumentException("length <= 0");
}
final int bufferLength = mBuffer.length;
synchronized (this) {
while (lengthToWrite > 0) {
while (bufferLength == mStoredBytes && mOpen) {
try {
wait();
} catch (InterruptedException e) {
// Ignore.
}
}
if (!mOpen) return false;
final boolean wasEmpty = mStoredBytes == 0;
int bytesToWriteBeforeWaiting = Math.min(lengthToWrite, bufferLength - mStoredBytes);
lengthToWrite -= bytesToWriteBeforeWaiting;
while (bytesToWriteBeforeWaiting > 0) {
int tail = mHead + mStoredBytes;
int oneRun;
if (tail >= bufferLength) {
// Buffer: [.............]
// ________________H_______T
// =>
// Buffer: [.............]
// ___________T____H
// onRun= _____----_
tail = tail - bufferLength;
oneRun = mHead - tail;
} else {
oneRun = bufferLength - tail;
}
int bytesToCopy = Math.min(oneRun, bytesToWriteBeforeWaiting);
System.arraycopy(buffer, offset, mBuffer, tail, bytesToCopy);
offset += bytesToCopy;
bytesToWriteBeforeWaiting -= bytesToCopy;
mStoredBytes += bytesToCopy;
}
if (wasEmpty) notify();
}
}
return true;
}
}

View File

@@ -4,7 +4,7 @@ import android.util.Log;
public final class EmulatorDebug { public final class EmulatorDebug {
/** The tag to use with {@link Log}. */ /** The tag to use with {@link Log}. */
public static final String LOG_TAG = "termux"; public static final String LOG_TAG = "termux";
} }

View File

@@ -0,0 +1,41 @@
package com.termux.terminal;
/**
* Native methods for creating and managing pseudoterminal subprocesses. C code is in jni/termux.c.
*/
final class JNI {
static {
System.loadLibrary("termux");
}
/**
* Create a subprocess. Differs from {@link ProcessBuilder} in that a pseudoterminal is used to communicate with the
* subprocess.
* <p/>
* Callers are responsible for calling {@link #close(int)} on the returned file descriptor.
*
* @param cmd The command to execute
* @param cwd The current working directory for the executed command
* @param args An array of arguments to the command
* @param envVars An array of strings of the form "VAR=value" to be added to the environment of the process
* @param processId A one-element array to which the process ID of the started process will be written.
* @return the file descriptor resulting from opening /dev/ptmx master device. The sub process will have opened the
* slave device counterpart (/dev/pts/$N) and have it as stdint, stdout and stderr.
*/
public static native int createSubprocess(String cmd, String cwd, String[] args, String[] envVars, int[] processId, int rows, int columns);
/** Set the window size for a given pty, which allows connected programs to learn how large their screen is. */
public static native void setPtyWindowSize(int fd, int rows, int cols);
/**
* Causes the calling thread to wait for the process associated with the receiver to finish executing.
*
* @return if >= 0, the exit status of the process. If < 0, the signal causing the process to stop negated.
*/
public static native int waitFor(int processId);
/** Close a file descriptor through the close(2) system call. */
public static native void close(int fileDescriptor);
}

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