Compare commits
227 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f9771adce | ||
|
|
b34f60b1b0 | ||
|
|
0fe608f91e | ||
|
|
5d911ef93f | ||
|
|
1d06ff9bf0 | ||
|
|
107927f5a1 | ||
|
|
d6eb5e3511 | ||
|
|
a6ae656c9f | ||
|
|
3af5730354 | ||
|
|
3306c3c2a2 | ||
|
|
cde0bd2246 | ||
|
|
354fe1948e | ||
|
|
ae1c9bacd6 | ||
|
|
d19cf435be | ||
|
|
1132028bd2 | ||
|
|
b7b4a0a8a2 | ||
|
|
ffd8687b4c | ||
|
|
bbb6f4471f | ||
|
|
b33b906784 | ||
|
|
567eeca782 | ||
|
|
3d46849673 | ||
|
|
6293f5f170 | ||
|
|
e5c5174f6f | ||
|
|
f1034c2e79 | ||
|
|
bbf03a0507 | ||
|
|
153818f7fb | ||
|
|
192b208883 | ||
|
|
824b3e657f | ||
|
|
a95e187b25 | ||
|
|
f888f35e35 | ||
|
|
24a5493ea5 | ||
|
|
0cd7cb8162 | ||
|
|
df4d8ac7e5 | ||
|
|
64fb2ce49b | ||
|
|
0c9b85a4f9 | ||
|
|
62a2104adc | ||
|
|
71dfefd4b7 | ||
|
|
682ce08314 | ||
|
|
c9a476caf7 | ||
|
|
9cee71004f | ||
|
|
81d97c3584 | ||
|
|
939338aaac | ||
|
|
067709bf4b | ||
|
|
69e4b575a8 | ||
|
|
07e6ecd3c3 | ||
|
|
325a6f7d66 | ||
|
|
cf5bb69fc8 | ||
|
|
18b004a2ba | ||
|
|
38323b1c2a | ||
|
|
8a5442f80d | ||
|
|
8598b92dea | ||
|
|
b2cd20c035 | ||
|
|
d4fc34ca2d | ||
|
|
c0323fe816 | ||
|
|
a32309827f | ||
|
|
ada678dfe2 | ||
|
|
cdbd38faaa | ||
|
|
d4653d0590 | ||
|
|
49f53f55f3 | ||
|
|
15eb56d4dd | ||
|
|
d7ea770d47 | ||
|
|
2a8d5e292d | ||
|
|
04da4b2268 | ||
|
|
34bacfd5b1 | ||
|
|
006d5abb78 | ||
|
|
480bad181b | ||
|
|
2afa4b4351 | ||
|
|
a2209ddd5e | ||
|
|
2cc6285a81 | ||
|
|
5dee839230 | ||
|
|
1ef8eb9219 | ||
|
|
d3ddb21716 | ||
|
|
8e80e889f0 | ||
|
|
4e5d14e4a2 | ||
|
|
0230698494 | ||
|
|
977cb34fc7 | ||
|
|
f62febbfb7 | ||
|
|
7ca20fdeb3 | ||
|
|
922f4f4ae5 | ||
|
|
df03f0b7d6 | ||
|
|
249f7c6b7c | ||
|
|
78a99fddfd | ||
|
|
dff2794501 | ||
|
|
3e0f74a894 | ||
|
|
2b3f681723 | ||
|
|
d3d6731d6f | ||
|
|
9bbcc08ff3 | ||
|
|
7aa957c684 | ||
|
|
eeb8554535 | ||
|
|
4eced52c5f | ||
|
|
b856e16998 | ||
|
|
1bdf9bf2e3 | ||
|
|
92b804dc9c | ||
|
|
1b5e5b56cb | ||
|
|
31371b5e3d | ||
|
|
ef1ab197b6 | ||
|
|
bccc35bc3f | ||
|
|
20d20f42c0 | ||
|
|
9d36e9adde | ||
|
|
3491956385 | ||
|
|
134e21765c | ||
|
|
c28990a176 | ||
|
|
131f481750 | ||
|
|
f393e9b2cf | ||
|
|
8612a1d0f8 | ||
|
|
7d53a147d8 | ||
|
|
5d6a98452a | ||
|
|
b4995ef9a7 | ||
|
|
ec7568d28e | ||
|
|
607ba3ee56 | ||
|
|
ae260fad9c | ||
|
|
d9b5344b24 | ||
|
|
bc02508102 | ||
|
|
f969c01f7e | ||
|
|
fb6e9b69ab | ||
|
|
c1a377d15d | ||
|
|
8c1c7ce727 | ||
|
|
2a940dfca6 | ||
|
|
2e7fd480f4 | ||
|
|
9e82561804 | ||
|
|
4427c1675d | ||
|
|
6f6d11fbb8 | ||
|
|
ff0440d7d2 | ||
|
|
c9e18e5b93 | ||
|
|
5e0b29bb6d | ||
|
|
73af5c2326 | ||
|
|
7da847a485 | ||
|
|
11a236a172 | ||
|
|
3b5d3114a6 | ||
|
|
66f15d2a08 | ||
|
|
0225a8b1fc | ||
|
|
d39972b3bf | ||
|
|
93b506a001 | ||
|
|
ebf2e472b3 | ||
|
|
e72841ba27 | ||
|
|
5fd91b4f92 | ||
|
|
10d6eaa5d1 | ||
|
|
319446fc15 | ||
|
|
36be41d0de | ||
|
|
ef9e406300 | ||
|
|
7b4acb53c9 | ||
|
|
dbf84773d4 | ||
|
|
324a69f7fa | ||
|
|
14c49867f7 | ||
|
|
395759cc0a | ||
|
|
ada5087f67 | ||
|
|
93a5bf8d29 | ||
|
|
356a442c6f | ||
|
|
9fd2cf9834 | ||
|
|
5a96075025 | ||
|
|
85b2c44ac7 | ||
|
|
108e4cb391 | ||
|
|
80858bab6b | ||
|
|
00194ebb90 | ||
|
|
f50d15d353 | ||
|
|
edcebf1336 | ||
|
|
fcfd131ccd | ||
|
|
ce5e9a9042 | ||
|
|
7884cb3bea | ||
|
|
d8fcc1f221 | ||
|
|
397a78f248 | ||
|
|
8baf53b09a | ||
|
|
4139bf9424 | ||
|
|
94deecb7b1 | ||
|
|
a4381b7827 | ||
|
|
866da75fa9 | ||
|
|
2b6e9ade07 | ||
|
|
6d1b0efd3b | ||
|
|
e03858f065 | ||
|
|
01929397cf | ||
|
|
496da3f877 | ||
|
|
407e4e003a | ||
|
|
fec61d315f | ||
|
|
f3a3a89f93 | ||
|
|
5b084f0851 | ||
|
|
a7eb173178 | ||
|
|
fa5117a098 | ||
|
|
2ef45c85b2 | ||
|
|
58b7a26b33 | ||
|
|
6e2a2ed946 | ||
|
|
7be1fe5555 | ||
|
|
0e94d52094 | ||
|
|
096dedffb1 | ||
|
|
b5d491a54c | ||
|
|
f6822d6c24 | ||
|
|
32c3ffd57b | ||
|
|
92570bee06 | ||
|
|
05bb399893 | ||
|
|
fe584940e1 | ||
|
|
78cdaef6d2 | ||
|
|
7b4a69f839 | ||
|
|
831aa69da8 | ||
|
|
a56cba6843 | ||
|
|
9228982632 | ||
|
|
38114784f1 | ||
|
|
b805f1486c | ||
|
|
7d31b7f480 | ||
|
|
a0298285e3 | ||
|
|
538a1d5cdf | ||
|
|
f1e973f0d2 | ||
|
|
b467b68f7b | ||
|
|
b895cbbb1e | ||
|
|
fc30eba247 | ||
|
|
b1d4c0c7fe | ||
|
|
7fe5bd32c8 | ||
|
|
43bbef9a11 | ||
|
|
eaea0f74a5 | ||
|
|
cb13a5a531 | ||
|
|
d267843e36 | ||
|
|
5ca67dd885 | ||
|
|
6b0d531758 | ||
|
|
1b3283bd69 | ||
|
|
2820f6a7b8 | ||
|
|
8925ae67bb | ||
|
|
4066c5df42 | ||
|
|
20aac6aa72 | ||
|
|
e1f799f9a1 | ||
|
|
4479de4b9b | ||
|
|
3a8f53a54c | ||
|
|
2f04a6186b | ||
|
|
ad64dd7c3d | ||
|
|
dfc4595ec5 | ||
|
|
8c80efb904 | ||
|
|
564079c7e9 | ||
|
|
5b6fd9b88c | ||
|
|
63bfe95848 | ||
|
|
3bb2849a88 |
8
.gitattributes
vendored
@@ -1,3 +1,5 @@
|
|||||||
* text=auto
|
* text=auto
|
||||||
*.bat eol=crlf
|
*.bat eol=crlf
|
||||||
*.sh eol=lf
|
*.gradle eol=lf
|
||||||
|
*.mk eol=lf
|
||||||
|
*.sh eol=lf
|
||||||
|
|||||||
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
patreon: termux
|
||||||
|
custom: https://paypal.me/fornwall
|
||||||
4
.github/workflows/debug_build.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
./gradlew assembleDebug
|
./gradlew assembleDebug
|
||||||
- name: Store generated APK file
|
- name: Store generated APK file
|
||||||
uses: actions/upload-artifact@v1
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: termux-app
|
name: termux-app
|
||||||
path: ./app/build/outputs/apk/debug/app-debug.apk
|
path: ./app/build/outputs/apk/debug
|
||||||
|
|||||||
26
.github/workflows/publish_libraries.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
name: Publish library packages
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
paths:
|
||||||
|
- 'terminal-emulator/build.gradle'
|
||||||
|
- 'terminal-view/build.gradle'
|
||||||
|
- 'termux-shared/build.gradle'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Clone repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Perform release build
|
||||||
|
run: |
|
||||||
|
./gradlew assembleRelease
|
||||||
|
- name: Publish libraries on Github Packages
|
||||||
|
env:
|
||||||
|
GH_USERNAME: xeffyr
|
||||||
|
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
|
run: |
|
||||||
|
./gradlew publish
|
||||||
15
README.md
@@ -14,13 +14,18 @@ Note that this repository is for the app itself (the user interface and the
|
|||||||
terminal emulation). For the packages installable inside the app, see
|
terminal emulation). For the packages installable inside the app, see
|
||||||
[termux/termux-packages](https://github.com/termux/termux-packages)
|
[termux/termux-packages](https://github.com/termux/termux-packages)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
**@termux is looking for Termux Application maintainer for implementing new features,
|
||||||
|
fixing bugs and reviewing pull requests since current one (@fornwall) is inactive.**
|
||||||
|
|
||||||
|
Issue https://github.com/termux/termux-app/issues/1072 needs extra attention.
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Termux application can be obtained from:
|
Termux application can be obtained from [F-Droid](https://f-droid.org/en/packages/com.termux/).
|
||||||
|
|
||||||
- [Google Play](https://play.google.com/store/apps/details?id=com.termux)
|
|
||||||
- [F-Droid](https://f-droid.org/en/packages/com.termux/)
|
|
||||||
- [Kali Nethunter Store](https://store.nethunter.com/en/packages/com.termux/)
|
|
||||||
|
|
||||||
Additionally we provide per-commit debug builds for those who want to try
|
Additionally we provide per-commit debug builds for those who want to try
|
||||||
out the latest features or test their pull request. This build can be obtained
|
out the latest features or test their pull request. This build can be obtained
|
||||||
|
|||||||
@@ -7,18 +7,36 @@ android {
|
|||||||
ndkVersion project.properties.ndkVersion
|
ndkVersion project.properties.ndkVersion
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "androidx.annotation:annotation:1.1.0"
|
implementation "androidx.annotation:annotation:1.2.0"
|
||||||
|
implementation "androidx.core:core:1.5.0-rc01"
|
||||||
|
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
|
||||||
|
implementation "androidx.preference:preference:1.1.1"
|
||||||
implementation "androidx.viewpager:viewpager:1.0.0"
|
implementation "androidx.viewpager:viewpager:1.0.0"
|
||||||
implementation "androidx.drawerlayout:drawerlayout:1.1.0"
|
implementation "com.google.guava:guava:24.1-jre"
|
||||||
|
implementation "io.noties.markwon:core:$markwonVersion"
|
||||||
|
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
|
||||||
|
implementation "io.noties.markwon:linkify:$markwonVersion"
|
||||||
|
implementation "io.noties.markwon:recycler:$markwonVersion"
|
||||||
|
|
||||||
implementation project(":terminal-view")
|
implementation project(":terminal-view")
|
||||||
|
implementation project(":termux-shared")
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.termux"
|
applicationId "com.termux"
|
||||||
minSdkVersion project.properties.minSdkVersion.toInteger()
|
minSdkVersion project.properties.minSdkVersion.toInteger()
|
||||||
targetSdkVersion project.properties.targetSdkVersion.toInteger()
|
targetSdkVersion project.properties.targetSdkVersion.toInteger()
|
||||||
versionCode 100
|
versionCode project.properties.termuxVersionCode.toInteger()
|
||||||
versionName "0.100"
|
versionName project.properties.termuxVersion
|
||||||
|
|
||||||
|
manifestPlaceholders.TERMUX_PACKAGE_NAME = "com.termux"
|
||||||
|
manifestPlaceholders.TERMUX_APP_NAME = "Termux"
|
||||||
|
manifestPlaceholders.TERMUX_API_APP_NAME = "Termux:API"
|
||||||
|
manifestPlaceholders.TERMUX_BOOT_APP_NAME = "Termux:Boot"
|
||||||
|
manifestPlaceholders.TERMUX_FLOAT_APP_NAME = "Termux:Float"
|
||||||
|
manifestPlaceholders.TERMUX_STYLING_APP_NAME = "Termux:Styling"
|
||||||
|
manifestPlaceholders.TERMUX_TASKER_APP_NAME = "Termux:Tasker"
|
||||||
|
manifestPlaceholders.TERMUX_WIDGET_APP_NAME = "Termux:Widget"
|
||||||
|
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
ndkBuild {
|
ndkBuild {
|
||||||
@@ -64,6 +82,10 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
disable 'ProtectedPermissions'
|
||||||
|
}
|
||||||
|
|
||||||
testOptions {
|
testOptions {
|
||||||
unitTests {
|
unitTests {
|
||||||
includeAndroidResources = true
|
includeAndroidResources = true
|
||||||
@@ -72,8 +94,8 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation 'junit:junit:4.13'
|
testImplementation "junit:junit:4.13.2"
|
||||||
testImplementation 'org.robolectric:robolectric:4.3.1'
|
testImplementation "org.robolectric:robolectric:4.4"
|
||||||
}
|
}
|
||||||
|
|
||||||
task versionName {
|
task versionName {
|
||||||
@@ -82,7 +104,7 @@ task versionName {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def downloadBootstrap(String arch, String expectedChecksum, int version) {
|
def downloadBootstrap(String arch, String expectedChecksum, String version) {
|
||||||
def digest = java.security.MessageDigest.getInstance("SHA-256")
|
def digest = java.security.MessageDigest.getInstance("SHA-256")
|
||||||
|
|
||||||
def localUrl = "src/main/cpp/bootstrap-" + arch + ".zip"
|
def localUrl = "src/main/cpp/bootstrap-" + arch + ".zip"
|
||||||
@@ -104,7 +126,7 @@ def downloadBootstrap(String arch, String expectedChecksum, int version) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def remoteUrl = "https://bintray.com/termux/bootstrap/download_file?file_path=bootstrap-" + arch + "-v" + version + ".zip"
|
def remoteUrl = "https://github.com/termux/termux-packages/releases/download/bootstrap-" + version + "/bootstrap-" + arch + ".zip"
|
||||||
logger.quiet("Downloading " + remoteUrl + " ...")
|
logger.quiet("Downloading " + remoteUrl + " ...")
|
||||||
|
|
||||||
file.parentFile.mkdirs()
|
file.parentFile.mkdirs()
|
||||||
@@ -131,13 +153,13 @@ clean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task downloadBootstraps(){
|
task downloadBootstraps() {
|
||||||
doLast {
|
doLast {
|
||||||
def version = 29
|
def version = "2021.04.13-r1"
|
||||||
downloadBootstrap("aarch64", "1b67ba2302efc20952c00d607a3a169e86cf8ecb66b0c6e16bee9997289119e1", version)
|
downloadBootstrap("aarch64", "ff82e5755d947cd1f3e0b30916d125c6ddd8ba3254801ca7499d73653417e158", version)
|
||||||
downloadBootstrap("arm", "dd87a5902b6b7ee4d4499a10da99a3ea605ba020ad1cc77cad16b3267ac13c2", version)
|
downloadBootstrap("arm", "53a7df2d6d0a36a8c9ab5259c8b5457c93b8bae8aec2321a470236b6da54e59a", version)
|
||||||
downloadBootstrap("i686", "66cf79273a2ee87730010febe258bec02abf4c4d4c6f89b1b174619baa3c5a0e", version)
|
downloadBootstrap("i686", "f0e1399a13ebed6c5229fde161f9848d9f5eeae7b8cd82f31250a813b52e371", version)
|
||||||
downloadBootstrap("x86_64", "e9541c02e4e7df4860d76098e4f477e09952ae65323216f692d668425842bf00", version)
|
downloadBootstrap("x86_64", "e36c4d8c933dc12b3f48937b7747c7a4dcfaa70f0dd89ad5e8b4465930075ae9", version)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
5
app/proguard-rules.pro
vendored
@@ -7,5 +7,6 @@
|
|||||||
# For more details, see
|
# For more details, see
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
-renamesourcefileattribute SourceFile
|
-dontobfuscate
|
||||||
-keepattributes SourceFile,LineNumberTable
|
#-renamesourcefileattribute SourceFile
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.termux"
|
package="com.termux"
|
||||||
android:installLocation="internalOnly"
|
android:installLocation="internalOnly"
|
||||||
android:sharedUserId="com.termux"
|
android:sharedUserId="${TERMUX_PACKAGE_NAME}"
|
||||||
android:sharedUserLabel="@string/shared_user_label" >
|
android:sharedUserLabel="@string/shared_user_label">
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
<uses-feature
|
||||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
android:name="android.hardware.touchscreen"
|
||||||
|
android:required="false" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.software.leanback"
|
||||||
|
android:required="false" />
|
||||||
|
|
||||||
<permission android:name="com.termux.permission.RUN_COMMAND"
|
<permission
|
||||||
android:label="@string/run_command_permission_label"
|
android:name="${TERMUX_PACKAGE_NAME}.permission.RUN_COMMAND"
|
||||||
android:description="@string/run_command_permission_description"
|
android:description="@string/permission_run_command_description"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/permission_run_command_label"
|
||||||
android:protectionLevel="dangerous" />
|
android:protectionLevel="dangerous" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
@@ -20,59 +26,102 @@
|
|||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||||
|
<uses-permission android:name="android.permission.READ_LOGS" />
|
||||||
|
<uses-permission android:name="android.permission.DUMP" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:extractNativeLibs="true"
|
android:name=".app.TermuxApplication"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:icon="@drawable/ic_launcher"
|
|
||||||
android:banner="@drawable/banner"
|
android:banner="@drawable/banner"
|
||||||
|
android:extractNativeLibs="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/application_name"
|
android:label="@string/application_name"
|
||||||
android:theme="@style/Theme.Termux"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="false" >
|
android:supportsRtl="false"
|
||||||
|
android:theme="@style/Theme.Termux">
|
||||||
|
|
||||||
<!-- This (or rather, value 2.1 or higher) is needed to make the Samsung Galaxy S8
|
<!--
|
||||||
mark the app with "This app is optimized to run in full screen." -->
|
This (or rather, value 2.1 or higher) is needed to make the Samsung Galaxy S8
|
||||||
<meta-data android:name="android.max_aspect" android:value="10.0" />
|
mark the app with "This app is optimized to run in full screen."
|
||||||
|
-->
|
||||||
|
<meta-data
|
||||||
|
android:name="android.max_aspect"
|
||||||
|
android:value="10.0" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.termux.app.TermuxActivity"
|
android:name=".app.TermuxActivity"
|
||||||
android:label="@string/application_name"
|
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|uiMode|keyboard|keyboardHidden|navigation"
|
||||||
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|uiMode|keyboard|keyboardHidden|navigation"
|
android:label="@string/application_name"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:windowSoftInputMode="adjustResize|stateAlwaysVisible" >
|
android:windowSoftInputMode="adjustResize|stateAlwaysVisible">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<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" />
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.app.shortcuts"
|
||||||
|
android:resource="@xml/shortcuts" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity-alias
|
||||||
android:name="com.termux.app.TermuxHelpActivity"
|
android:name=".HomeActivity"
|
||||||
android:exported="false"
|
android:targetActivity=".app.TermuxActivity">
|
||||||
android:theme="@android:style/Theme.Material.Light.DarkActionBar"
|
|
||||||
android:parentActivityName=".app.TermuxActivity"
|
<!-- Launch activity automatically on boot on Android Things devices -->
|
||||||
android:resizeableActivity="true"
|
<intent-filter>
|
||||||
android:label="@string/application_name" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.IOT_LAUNCHER" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity-alias>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.termux.filepicker.TermuxFileReceiverActivity"
|
android:name=".app.activities.HelpActivity"
|
||||||
|
android:exported="false"
|
||||||
android:label="@string/application_name"
|
android:label="@string/application_name"
|
||||||
android:taskAffinity="com.termux.filereceiver"
|
android:parentActivityName=".app.TermuxActivity"
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:noHistory="true">
|
android:theme="@android:style/Theme.Material.Light.DarkActionBar" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".app.activities.SettingsActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/title_activity_termux_settings"
|
||||||
|
android:theme="@style/Theme.AppCompat.Light.DarkActionBar" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".app.activities.ReportActivity"
|
||||||
|
android:theme="@style/Theme.AppCompat.TermuxReportActivity"
|
||||||
|
android:documentLaunchMode="intoExisting"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".filepicker.TermuxFileReceiverActivity"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:label="@string/application_name"
|
||||||
|
android:noHistory="true"
|
||||||
|
android:resizeableActivity="true"
|
||||||
|
android:taskAffinity="${TERMUX_PACKAGE_NAME}.filereceiver">
|
||||||
|
|
||||||
<!-- Accept multiple file types when sending. -->
|
<!-- Accept multiple file types when sending. -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<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="application/*" />
|
<data android:mimeType="application/*" />
|
||||||
<data android:mimeType="audio/*" />
|
<data android:mimeType="audio/*" />
|
||||||
<data android:mimeType="image/*" />
|
<data android:mimeType="image/*" />
|
||||||
@@ -81,36 +130,25 @@
|
|||||||
<data android:mimeType="text/*" />
|
<data android:mimeType="text/*" />
|
||||||
<data android:mimeType="video/*" />
|
<data android:mimeType="video/*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<!-- Be more restrictive for viewing files, restricting ourselves to text files. -->
|
<!-- Accept multiple file types to let Termux be usable as generic file viewer. -->
|
||||||
<intent-filter tools:ignore="AppLinkUrlError">
|
<intent-filter tools:ignore="AppLinkUrlError">
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
|
<data android:mimeType="application/*" />
|
||||||
|
<data android:mimeType="audio/*" />
|
||||||
|
<data android:mimeType="image/*" />
|
||||||
<data android:mimeType="text/*" />
|
<data android:mimeType="text/*" />
|
||||||
<data android:mimeType="application/*log*" />
|
<data android:mimeType="video/*" />
|
||||||
<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>
|
||||||
|
|
||||||
<activity-alias
|
|
||||||
android:name=".HomeActivity"
|
|
||||||
android:targetActivity="com.termux.app.TermuxActivity">
|
|
||||||
|
|
||||||
<!-- Launch activity automatically on boot on Android Things devices -->
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
|
||||||
<category android:name="android.intent.category.IOT_LAUNCHER"/>
|
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
|
||||||
</intent-filter>
|
|
||||||
</activity-alias>
|
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name=".filepicker.TermuxDocumentsProvider"
|
android:name=".filepicker.TermuxDocumentsProvider"
|
||||||
android:authorities="com.termux.documents"
|
android:authorities="${TERMUX_PACKAGE_NAME}.documents"
|
||||||
android:grantUriPermissions="true"
|
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
android:grantUriPermissions="true"
|
||||||
android:permission="android.permission.MANAGE_DOCUMENTS">
|
android:permission="android.permission.MANAGE_DOCUMENTS">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
||||||
@@ -118,27 +156,32 @@
|
|||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="com.termux.app.TermuxService"
|
android:name=".app.TermuxService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".app.RunCommandService"
|
android:name=".app.RunCommandService"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:permission="com.termux.permission.RUN_COMMAND" >
|
android:permission="${TERMUX_PACKAGE_NAME}.permission.RUN_COMMAND">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.termux.RUN_COMMAND" />
|
<action android:name="${TERMUX_PACKAGE_NAME}.RUN_COMMAND" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<receiver android:name=".app.TermuxOpenReceiver" />
|
<receiver android:name=".app.TermuxOpenReceiver" />
|
||||||
|
|
||||||
<provider android:authorities="com.termux.files"
|
<provider
|
||||||
android:readPermission="android.permission.permRead"
|
android:name=".app.TermuxOpenReceiver$ContentProvider"
|
||||||
android:exported="true"
|
android:authorities="${TERMUX_PACKAGE_NAME}.files"
|
||||||
android:grantUriPermissions="true"
|
android:exported="true"
|
||||||
android:name="com.termux.app.TermuxOpenReceiver$ContentProvider" />
|
android:grantUriPermissions="true"
|
||||||
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
android:readPermission="android.permission.permRead" />
|
||||||
<meta-data android:name="com.samsung.android.multidisplay.keep_process_alive" android:value="true"/>
|
|
||||||
|
<meta-data
|
||||||
|
android:name="com.sec.android.support.multiwindow"
|
||||||
|
android:value="true" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.samsung.android.multidisplay.keep_process_alive"
|
||||||
|
android:value="true" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,240 +0,0 @@
|
|||||||
package com.termux.app;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
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){
|
|
||||||
this(cwd, fileToExecute, args, service, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public BackgroundJob(String cwd, String fileToExecute, final String[] args, final TermuxService service, PendingIntent pendingIntent) {
|
|
||||||
String[] env = buildEnvironment(false, cwd);
|
|
||||||
if (cwd == null) cwd = TermuxService.HOME_PATH;
|
|
||||||
|
|
||||||
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);
|
|
||||||
final Bundle result = new Bundle();
|
|
||||||
final StringBuilder outResult = new StringBuilder();
|
|
||||||
final StringBuilder errResult = new StringBuilder();
|
|
||||||
|
|
||||||
Thread errThread = new Thread() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
InputStream stderr = mProcess.getErrorStream();
|
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(stderr, StandardCharsets.UTF_8));
|
|
||||||
String line;
|
|
||||||
try {
|
|
||||||
// FIXME: Long lines.
|
|
||||||
while ((line = reader.readLine()) != null) {
|
|
||||||
errResult.append(line).append('\n');
|
|
||||||
Log.i(LOG_TAG, "[" + pid + "] stderr: " + line);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
// Ignore.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
errThread.start();
|
|
||||||
|
|
||||||
new Thread() {
|
|
||||||
@Override
|
|
||||||
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);
|
|
||||||
outResult.append(line).append('\n');
|
|
||||||
}
|
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
result.putString("stdout", outResult.toString());
|
|
||||||
result.putInt("exitCode", exitCode);
|
|
||||||
|
|
||||||
errThread.join();
|
|
||||||
result.putString("stderr", errResult.toString());
|
|
||||||
|
|
||||||
Intent data = new Intent();
|
|
||||||
data.putExtra("result", result);
|
|
||||||
|
|
||||||
if(pendingIntent != null) {
|
|
||||||
try {
|
|
||||||
pendingIntent.send(service.getApplicationContext(), Activity.RESULT_OK, data);
|
|
||||||
} catch (PendingIntent.CanceledException e) {
|
|
||||||
// The caller doesn't want the result? That's fine, just ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
// Ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void addToEnvIfPresent(List<String> environment, String name) {
|
|
||||||
String value = System.getenv(name);
|
|
||||||
if (value != null) {
|
|
||||||
environment.add(name + "=" + value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static String[] buildEnvironment(boolean failSafe, String cwd) {
|
|
||||||
new File(TermuxService.HOME_PATH).mkdirs();
|
|
||||||
|
|
||||||
if (cwd == null) cwd = TermuxService.HOME_PATH;
|
|
||||||
|
|
||||||
List<String> environment = new ArrayList<>();
|
|
||||||
|
|
||||||
environment.add("TERM=xterm-256color");
|
|
||||||
environment.add("COLORTERM=truecolor");
|
|
||||||
environment.add("HOME=" + TermuxService.HOME_PATH);
|
|
||||||
environment.add("PREFIX=" + TermuxService.PREFIX_PATH);
|
|
||||||
environment.add("BOOTCLASSPATH=" + System.getenv("BOOTCLASSPATH"));
|
|
||||||
environment.add("ANDROID_ROOT=" + System.getenv("ANDROID_ROOT"));
|
|
||||||
environment.add("ANDROID_DATA=" + System.getenv("ANDROID_DATA"));
|
|
||||||
// EXTERNAL_STORAGE is needed for /system/bin/am to work on at least
|
|
||||||
// Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3.
|
|
||||||
environment.add("EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE"));
|
|
||||||
|
|
||||||
// These variables are needed if running on Android 10 and higher.
|
|
||||||
addToEnvIfPresent(environment, "ANDROID_ART_ROOT");
|
|
||||||
addToEnvIfPresent(environment, "DEX2OATBOOTCLASSPATH");
|
|
||||||
addToEnvIfPresent(environment, "ANDROID_I18N_ROOT");
|
|
||||||
addToEnvIfPresent(environment, "ANDROID_RUNTIME_ROOT");
|
|
||||||
addToEnvIfPresent(environment, "ANDROID_TZDATA_ROOT");
|
|
||||||
|
|
||||||
if (failSafe) {
|
|
||||||
// Keep the default path so that system binaries can be used in the failsafe session.
|
|
||||||
environment.add("PATH= " + System.getenv("PATH"));
|
|
||||||
} else {
|
|
||||||
environment.add("LANG=en_US.UTF-8");
|
|
||||||
environment.add("PATH=" + TermuxService.PREFIX_PATH + "/bin:" + TermuxService.PREFIX_PATH + "/bin/applets");
|
|
||||||
environment.add("PWD=" + cwd);
|
|
||||||
environment.add("TMPDIR=" + TermuxService.PREFIX_PATH + "/tmp");
|
|
||||||
}
|
|
||||||
|
|
||||||
return environment.toArray(new String[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
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[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,75 +1,35 @@
|
|||||||
package com.termux.app;
|
package com.termux.app;
|
||||||
|
|
||||||
import android.app.Notification;
|
import android.app.Notification;
|
||||||
import android.app.NotificationChannel;
|
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
import java.io.File;
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE;
|
||||||
import java.io.FileInputStream;
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||||
import java.io.InputStreamReader;
|
import com.termux.shared.file.FileUtils;
|
||||||
import java.nio.charset.StandardCharsets;
|
import com.termux.shared.logger.Logger;
|
||||||
import java.util.Properties;
|
import com.termux.shared.notification.NotificationUtils;
|
||||||
|
import com.termux.app.utils.PluginUtils;
|
||||||
|
import com.termux.shared.data.DataUtils;
|
||||||
|
import com.termux.shared.models.ExecutionCommand;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When allow-external-apps property is set to "true" in ~/.termux/termux.properties, Termux
|
* A service that receives {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent from third party apps and
|
||||||
* is able to process execute intents sent by third-party applications.
|
* plugins that contains info on command execution and forwards the extras to {@link TermuxService}
|
||||||
|
* for the actual execution.
|
||||||
*
|
*
|
||||||
* Third-party program must declare com.termux.permission.RUN_COMMAND permission and it should be
|
* Check https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent for more info.
|
||||||
* granted by user.
|
|
||||||
*
|
|
||||||
* Absolute path of command or script must be given in "RUN_COMMAND_PATH" extra.
|
|
||||||
* The "RUN_COMMAND_ARGUMENTS", "RUN_COMMAND_WORKDIR" and "RUN_COMMAND_BACKGROUND" extras are
|
|
||||||
* optional. The workdir defaults to termux home. The background mode defaults to "false".
|
|
||||||
* The command path and workdir can optionally be prefixed with "$PREFIX/" or "~/" if an absolute
|
|
||||||
* path is not to be given.
|
|
||||||
*
|
|
||||||
* To automatically bring to foreground and start termux commands that were started with
|
|
||||||
* background mode "false" in android >= 10 without user having to click the notification manually,
|
|
||||||
* requires termux to be granted draw over apps permission due to new restrictions
|
|
||||||
* of starting activities from the background, this also applies to Termux:Tasker plugin.
|
|
||||||
*
|
|
||||||
* To reduce the chance of termux being killed by android even further due to violation of not
|
|
||||||
* being able to call startForeground() within ~5s of service start in android >= 8, the user
|
|
||||||
* may disable battery optimizations for termux.
|
|
||||||
*
|
|
||||||
* Sample code to run command "top" with java:
|
|
||||||
* Intent intent = new Intent();
|
|
||||||
* intent.setClassName("com.termux", "com.termux.app.RunCommandService");
|
|
||||||
* intent.setAction("com.termux.RUN_COMMAND");
|
|
||||||
* intent.putExtra("com.termux.RUN_COMMAND_PATH", "/data/data/com.termux/files/usr/bin/top");
|
|
||||||
* intent.putExtra("com.termux.RUN_COMMAND_ARGUMENTS", new String[]{"-n", "5"});
|
|
||||||
* intent.putExtra("com.termux.RUN_COMMAND_WORKDIR", "/data/data/com.termux/files/home");
|
|
||||||
* intent.putExtra("com.termux.RUN_COMMAND_BACKGROUND", false);
|
|
||||||
* startService(intent);
|
|
||||||
*
|
|
||||||
* Sample code to run command "top" with "am startservice" command:
|
|
||||||
* am startservice --user 0 -n com.termux/com.termux.app.RunCommandService
|
|
||||||
* -a com.termux.RUN_COMMAND
|
|
||||||
* --es com.termux.RUN_COMMAND_PATH '/data/data/com.termux/files/usr/bin/top'
|
|
||||||
* --esa com.termux.RUN_COMMAND_ARGUMENTS '-n,5'
|
|
||||||
* --es com.termux.RUN_COMMAND_WORKDIR '/data/data/com.termux/files/home'
|
|
||||||
* --ez com.termux.RUN_COMMAND_BACKGROUND 'false'
|
|
||||||
*/
|
*/
|
||||||
public class RunCommandService extends Service {
|
public class RunCommandService extends Service {
|
||||||
|
|
||||||
public static final String RUN_COMMAND_ACTION = "com.termux.RUN_COMMAND";
|
private static final String LOG_TAG = "RunCommandService";
|
||||||
public static final String RUN_COMMAND_PATH = "com.termux.RUN_COMMAND_PATH";
|
|
||||||
public static final String RUN_COMMAND_ARGUMENTS = "com.termux.RUN_COMMAND_ARGUMENTS";
|
|
||||||
public static final String RUN_COMMAND_WORKDIR = "com.termux.RUN_COMMAND_WORKDIR";
|
|
||||||
public static final String RUN_COMMAND_BACKGROUND = "com.termux.RUN_COMMAND_BACKGROUND";
|
|
||||||
|
|
||||||
private static final String NOTIFICATION_CHANNEL_ID = "termux_run_command_notification_channel";
|
|
||||||
private static final int NOTIFICATION_ID = 1338;
|
|
||||||
|
|
||||||
class LocalBinder extends Binder {
|
class LocalBinder extends Binder {
|
||||||
public final RunCommandService service = RunCommandService.this;
|
public final RunCommandService service = RunCommandService.this;
|
||||||
@@ -84,30 +44,133 @@ public class RunCommandService extends Service {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
|
Logger.logVerbose(LOG_TAG, "onCreate");
|
||||||
runStartForeground();
|
runStartForeground();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
|
Logger.logDebug(LOG_TAG, "onStartCommand");
|
||||||
|
|
||||||
|
if (intent == null) return Service.START_NOT_STICKY;
|
||||||
|
|
||||||
// Run again in case service is already started and onCreate() is not called
|
// Run again in case service is already started and onCreate() is not called
|
||||||
runStartForeground();
|
runStartForeground();
|
||||||
|
|
||||||
if (allowExternalApps() && RUN_COMMAND_ACTION.equals(intent.getAction())) {
|
ExecutionCommand executionCommand = new ExecutionCommand();
|
||||||
Uri programUri = new Uri.Builder().scheme("com.termux.file").path(parsePath(intent.getStringExtra(RUN_COMMAND_PATH))).build();
|
executionCommand.pluginAPIHelp = this.getString(R.string.error_run_command_service_api_help, RUN_COMMAND_SERVICE.RUN_COMMAND_API_HELP_URL);
|
||||||
|
|
||||||
Intent execIntent = new Intent(TermuxService.ACTION_EXECUTE, programUri);
|
String errmsg;
|
||||||
execIntent.setClass(this, TermuxService.class);
|
|
||||||
execIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, intent.getStringArrayExtra(RUN_COMMAND_ARGUMENTS));
|
|
||||||
execIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, parsePath(intent.getStringExtra(RUN_COMMAND_WORKDIR)));
|
|
||||||
execIntent.putExtra(TermuxService.EXTRA_EXECUTE_IN_BACKGROUND, intent.getBooleanExtra(RUN_COMMAND_BACKGROUND, false));
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
// If invalid action passed, then just return
|
||||||
this.startForegroundService(execIntent);
|
if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) {
|
||||||
} else {
|
errmsg = this.getString(R.string.error_run_command_service_invalid_intent_action, intent.getAction());
|
||||||
this.startService(execIntent);
|
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
||||||
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||||
|
return Service.START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
executionCommand.executable = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
|
||||||
|
executionCommand.arguments = intent.getStringArrayExtra(RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS);
|
||||||
|
executionCommand.stdin = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_STDIN);
|
||||||
|
executionCommand.workingDirectory = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_WORKDIR);
|
||||||
|
executionCommand.inBackground = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false);
|
||||||
|
executionCommand.sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION);
|
||||||
|
executionCommand.commandLabel = DataUtils.getDefaultIfNull(intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL), "RUN_COMMAND Execution Intent Command");
|
||||||
|
executionCommand.commandDescription = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION);
|
||||||
|
executionCommand.commandHelp = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_HELP);
|
||||||
|
executionCommand.isPluginExecutionCommand = true;
|
||||||
|
executionCommand.pluginPendingIntent = intent.getParcelableExtra(RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// If "allow-external-apps" property to not set to "true", then just return
|
||||||
|
// We enable force notifications if "allow-external-apps" policy is violated so that the
|
||||||
|
// user knows someone tried to run a command in termux context, since it may be malicious
|
||||||
|
// app or imported (tasker) plugin project and not the user himself. If a pending intent is
|
||||||
|
// also sent, then its creator is also logged and shown.
|
||||||
|
errmsg = PluginUtils.checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(this);
|
||||||
|
if (errmsg != null) {
|
||||||
|
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
||||||
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true);
|
||||||
|
return Service.START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// If executable is null or empty, then exit here instead of getting canonical path which would expand to "/"
|
||||||
|
if (executionCommand.executable == null || executionCommand.executable.isEmpty()) {
|
||||||
|
errmsg = this.getString(R.string.error_run_command_service_mandatory_extra_missing, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
|
||||||
|
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
||||||
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||||
|
return Service.START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get canonical path of executable
|
||||||
|
executionCommand.executable = FileUtils.getCanonicalPath(executionCommand.executable, null, true);
|
||||||
|
|
||||||
|
// If executable is not a regular file, or is not readable or executable, then just return
|
||||||
|
// Setting of missing read and execute permissions is not done
|
||||||
|
errmsg = FileUtils.validateRegularFileExistenceAndPermissions(this, "executable", executionCommand.executable, null,
|
||||||
|
PluginUtils.PLUGIN_EXECUTABLE_FILE_PERMISSIONS, true, true,
|
||||||
|
false);
|
||||||
|
if (errmsg != null) {
|
||||||
|
errmsg += "\n" + this.getString(R.string.msg_executable_absolute_path, executionCommand.executable);
|
||||||
|
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
||||||
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||||
|
return Service.START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// If workingDirectory is not null or empty
|
||||||
|
if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) {
|
||||||
|
// Get canonical path of workingDirectory
|
||||||
|
executionCommand.workingDirectory = FileUtils.getCanonicalPath(executionCommand.workingDirectory, null, true);
|
||||||
|
|
||||||
|
// If workingDirectory is not a directory, or is not readable or writable, then just return
|
||||||
|
// Creation of missing directory and setting of read, write and execute permissions are only done if workingDirectory is
|
||||||
|
// under {@link TermuxConstants#TERMUX_FILES_DIR_PATH}
|
||||||
|
// We try to set execute permissions, but ignore if they are missing, since only read and write permissions are required
|
||||||
|
// for working directories.
|
||||||
|
errmsg = FileUtils.validateDirectoryFileExistenceAndPermissions(this, "working", executionCommand.workingDirectory, TermuxConstants.TERMUX_FILES_DIR_PATH, true,
|
||||||
|
PluginUtils.PLUGIN_WORKING_DIRECTORY_PERMISSIONS, true, true,
|
||||||
|
true, true);
|
||||||
|
if (errmsg != null) {
|
||||||
|
errmsg += "\n" + this.getString(R.string.msg_working_directory_absolute_path, executionCommand.workingDirectory);
|
||||||
|
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
||||||
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||||
|
return Service.START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(FileUtils.getExpandedTermuxPath(executionCommand.executable)).build();
|
||||||
|
|
||||||
|
Logger.logVerbose(LOG_TAG, executionCommand.toString());
|
||||||
|
|
||||||
|
// Create execution intent with the action TERMUX_SERVICE#ACTION_SERVICE_EXECUTE to be sent to the TERMUX_SERVICE
|
||||||
|
Intent execIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, executionCommand.executableUri);
|
||||||
|
execIntent.setClass(this, TermuxService.class);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, executionCommand.arguments);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_STDIN, executionCommand.stdin);
|
||||||
|
if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) execIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, executionCommand.workingDirectory);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, executionCommand.inBackground);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION, executionCommand.sessionAction);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_LABEL, executionCommand.commandLabel);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, executionCommand.commandDescription);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_HELP, executionCommand.commandHelp);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP, executionCommand.pluginAPIHelp);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT, executionCommand.pluginPendingIntent);
|
||||||
|
|
||||||
|
// Start TERMUX_SERVICE and pass it execution intent
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
this.startForegroundService(execIntent);
|
||||||
|
} else {
|
||||||
|
this.startService(execIntent);
|
||||||
|
}
|
||||||
|
|
||||||
runStopForeground();
|
runStopForeground();
|
||||||
|
|
||||||
return Service.START_NOT_STICKY;
|
return Service.START_NOT_STICKY;
|
||||||
@@ -116,7 +179,7 @@ public class RunCommandService extends Service {
|
|||||||
private void runStartForeground() {
|
private void runStartForeground() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
setupNotificationChannel();
|
setupNotificationChannel();
|
||||||
startForeground(NOTIFICATION_ID, buildNotification());
|
startForeground(TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_ID, buildNotification());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,22 +190,21 @@ public class RunCommandService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Notification buildNotification() {
|
private Notification buildNotification() {
|
||||||
Notification.Builder builder = new Notification.Builder(this);
|
// Build the notification
|
||||||
builder.setContentTitle(getText(R.string.application_name) + " Run Command");
|
Notification.Builder builder = NotificationUtils.geNotificationBuilder(this,
|
||||||
builder.setSmallIcon(R.drawable.ic_service_notification);
|
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_LOW,
|
||||||
|
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_NAME, null, null,
|
||||||
// Use a low priority:
|
null, NotificationUtils.NOTIFICATION_MODE_SILENT);
|
||||||
builder.setPriority(Notification.PRIORITY_LOW);
|
if (builder == null) return null;
|
||||||
|
|
||||||
// No need to show a timestamp:
|
// No need to show a timestamp:
|
||||||
builder.setShowWhen(false);
|
builder.setShowWhen(false);
|
||||||
|
|
||||||
// Background color for small notification icon:
|
// Set notification icon
|
||||||
builder.setColor(0xFF607D8B);
|
builder.setSmallIcon(R.drawable.ic_service_notification);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
// Set background color for small notification icon
|
||||||
builder.setChannelId(NOTIFICATION_CHANNEL_ID);
|
builder.setColor(0xFF607D8B);
|
||||||
}
|
|
||||||
|
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
@@ -150,40 +212,8 @@ public class RunCommandService extends Service {
|
|||||||
private void setupNotificationChannel() {
|
private void setupNotificationChannel() {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
|
||||||
|
|
||||||
String channelName = "Termux Run Command";
|
NotificationUtils.setupNotificationChannel(this, TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_ID,
|
||||||
int importance = NotificationManager.IMPORTANCE_LOW;
|
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW);
|
||||||
|
|
||||||
NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, importance);
|
|
||||||
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
|
||||||
manager.createNotificationChannel(channel);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean allowExternalApps() {
|
|
||||||
File propsFile = new File(TermuxService.HOME_PATH + "/.termux/termux.properties");
|
|
||||||
if (!propsFile.exists())
|
|
||||||
propsFile = new File(TermuxService.HOME_PATH + "/.config/termux/termux.properties");
|
|
||||||
|
|
||||||
Properties props = new Properties();
|
|
||||||
try {
|
|
||||||
if (propsFile.isFile() && propsFile.canRead()) {
|
|
||||||
try (FileInputStream in = new FileInputStream(propsFile)) {
|
|
||||||
props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e("termux", "Error loading props", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return props.getProperty("allow-external-apps", "false").equals("true");
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Replace "$PREFIX/" or "~/" prefix with termux absolute paths */
|
|
||||||
private String parsePath(String path) {
|
|
||||||
if(path != null && !path.isEmpty()) {
|
|
||||||
path = path.replaceAll("^\\$PREFIX\\/", TermuxService.PREFIX_PATH + "/");
|
|
||||||
path = path.replaceAll("^~\\/", TermuxService.HOME_PATH + "/");
|
|
||||||
}
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
28
app/src/main/java/com/termux/app/TermuxApplication.java
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package com.termux.app;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
|
||||||
|
import com.termux.shared.crash.CrashHandler;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
|
||||||
|
|
||||||
|
public class TermuxApplication extends Application {
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
|
||||||
|
// Set crash handler for the app
|
||||||
|
CrashHandler.setCrashHandler(this);
|
||||||
|
|
||||||
|
// Set log level for the app
|
||||||
|
setLogLevel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setLogLevel() {
|
||||||
|
// Load the log level from shared preferences and set it to the {@link Logger.CURRENT_LOG_LEVEL}
|
||||||
|
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(getApplicationContext());
|
||||||
|
preferences.setLogLevel(null, preferences.getLogLevel());
|
||||||
|
Logger.logDebug("Starting Application");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -7,18 +7,18 @@ import android.content.Context;
|
|||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
import android.os.UserManager;
|
import android.os.UserManager;
|
||||||
import android.system.Os;
|
import android.system.Os;
|
||||||
import android.util.Log;
|
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.terminal.EmulatorDebug;
|
import com.termux.shared.file.FileUtils;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -29,11 +29,11 @@ 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/>
|
* <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 directory below.
|
||||||
* <p/>
|
* <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/>
|
* <p/>
|
||||||
* (3) A staging folder, $STAGING_PREFIX, is {@link #deleteFolder(File)} if left over from broken installation below.
|
* (3) A staging directory, $STAGING_PREFIX, is cleared if left over from broken installation below.
|
||||||
* <p/>
|
* <p/>
|
||||||
* (4) The zip file is loaded from a shared library.
|
* (4) The zip file is loaded from a shared library.
|
||||||
* <p/>
|
* <p/>
|
||||||
@@ -46,22 +46,37 @@ import java.util.zip.ZipInputStream;
|
|||||||
*/
|
*/
|
||||||
final class TermuxInstaller {
|
final class TermuxInstaller {
|
||||||
|
|
||||||
/** Performs setup if necessary. */
|
private static final String LOG_TAG = "TermuxInstaller";
|
||||||
static void setupIfNeeded(final Activity activity, final Runnable whenDone) {
|
|
||||||
|
/** Performs bootstrap setup if necessary. */
|
||||||
|
static void setupBootstrapIfNeeded(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:
|
||||||
UserManager um = (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)
|
String bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message, TermuxConstants.TERMUX_PREFIX_DIR_PATH);
|
||||||
|
Logger.logError(LOG_TAG, bootstrapErrorMessage);
|
||||||
|
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(bootstrapErrorMessage)
|
||||||
.setOnDismissListener(dialog -> System.exit(0)).setPositiveButton(android.R.string.ok, null).show();
|
.setOnDismissListener(dialog -> System.exit(0)).setPositiveButton(android.R.string.ok, null).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final File PREFIX_FILE = new File(TermuxService.PREFIX_PATH);
|
final String PREFIX_FILE_PATH = TermuxConstants.TERMUX_PREFIX_DIR_PATH;
|
||||||
if (PREFIX_FILE.isDirectory()) {
|
final File PREFIX_FILE = TermuxConstants.TERMUX_PREFIX_DIR;
|
||||||
whenDone.run();
|
|
||||||
return;
|
// If prefix directory exists, even if its a symlink to a valid directory and symlink is not broken/dangling
|
||||||
|
if (FileUtils.directoryFileExists(PREFIX_FILE_PATH, true)) {
|
||||||
|
File[] PREFIX_FILE_LIST = PREFIX_FILE.listFiles();
|
||||||
|
// If prefix directory is empty or only contains the tmp directory
|
||||||
|
if(PREFIX_FILE_LIST == null || PREFIX_FILE_LIST.length == 0 || (PREFIX_FILE_LIST.length == 1 && TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH.equals(PREFIX_FILE_LIST[0].getAbsolutePath()))) {
|
||||||
|
Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" exists but is empty or only contains the tmp directory.");
|
||||||
|
} else {
|
||||||
|
whenDone.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (FileUtils.fileExists(PREFIX_FILE_PATH, false)) {
|
||||||
|
Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" does not exist but another file exists at its destination.");
|
||||||
}
|
}
|
||||||
|
|
||||||
final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false);
|
final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false);
|
||||||
@@ -69,13 +84,27 @@ final class TermuxInstaller {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
final String STAGING_PREFIX_PATH = TermuxService.FILES_PATH + "/usr-staging";
|
Logger.logInfo(LOG_TAG, "Installing " + TermuxConstants.TERMUX_APP_NAME + " bootstrap packages.");
|
||||||
|
|
||||||
|
String errmsg;
|
||||||
|
|
||||||
|
final String STAGING_PREFIX_PATH = TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH;
|
||||||
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()) {
|
// Delete prefix staging directory or any file at its destination
|
||||||
deleteFolder(STAGING_PREFIX_FILE);
|
errmsg = FileUtils.deleteFile(activity, "prefix staging directory", STAGING_PREFIX_PATH, true);
|
||||||
|
if (errmsg != null) {
|
||||||
|
throw new RuntimeException(errmsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete prefix directory or any file at its destination
|
||||||
|
errmsg = FileUtils.deleteFile(activity, "prefix directory", PREFIX_FILE_PATH, true);
|
||||||
|
if (errmsg != null) {
|
||||||
|
throw new RuntimeException(errmsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + STAGING_PREFIX_PATH + "\".");
|
||||||
|
|
||||||
final byte[] buffer = new byte[8096];
|
final byte[] buffer = new byte[8096];
|
||||||
final List<Pair<String, String>> symlinks = new ArrayList<>(50);
|
final List<Pair<String, String>> symlinks = new ArrayList<>(50);
|
||||||
|
|
||||||
@@ -94,14 +123,14 @@ final class TermuxInstaller {
|
|||||||
String newPath = STAGING_PREFIX_PATH + "/" + parts[1];
|
String newPath = STAGING_PREFIX_PATH + "/" + parts[1];
|
||||||
symlinks.add(Pair.create(oldPath, newPath));
|
symlinks.add(Pair.create(oldPath, newPath));
|
||||||
|
|
||||||
ensureDirectoryExists(new File(newPath).getParentFile());
|
ensureDirectoryExists(activity, new File(newPath).getParentFile());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
String zipEntryName = zipEntry.getName();
|
String zipEntryName = zipEntry.getName();
|
||||||
File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName);
|
File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName);
|
||||||
boolean isDirectory = zipEntry.isDirectory();
|
boolean isDirectory = zipEntry.isDirectory();
|
||||||
|
|
||||||
ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile());
|
ensureDirectoryExists(activity, isDirectory ? targetFile : targetFile.getParentFile());
|
||||||
|
|
||||||
if (!isDirectory) {
|
if (!isDirectory) {
|
||||||
try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
|
try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
|
||||||
@@ -124,13 +153,16 @@ final class TermuxInstaller {
|
|||||||
Os.symlink(symlink.first, symlink.second);
|
Os.symlink(symlink.first, symlink.second);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.logInfo(LOG_TAG, "Moving prefix staging to prefix directory.");
|
||||||
|
|
||||||
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("Moving prefix staging to prefix directory failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.logInfo(LOG_TAG, "Bootstrap packages installed successfully.");
|
||||||
activity.runOnUiThread(whenDone);
|
activity.runOnUiThread(whenDone);
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
Log.e(EmulatorDebug.LOG_TAG, "Bootstrap error", e);
|
Logger.logStackTraceWithMessage(LOG_TAG, "Bootstrap error", e);
|
||||||
activity.runOnUiThread(() -> {
|
activity.runOnUiThread(() -> {
|
||||||
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)
|
||||||
@@ -138,9 +170,9 @@ final class TermuxInstaller {
|
|||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
activity.finish();
|
activity.finish();
|
||||||
}).setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
|
}).setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
TermuxInstaller.setupIfNeeded(activity, whenDone);
|
TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone);
|
||||||
}).show();
|
}).show();
|
||||||
} catch (WindowManager.BadTokenException e1) {
|
} catch (WindowManager.BadTokenException e1) {
|
||||||
// Activity already dismissed - ignore.
|
// Activity already dismissed - ignore.
|
||||||
}
|
}
|
||||||
@@ -158,58 +190,25 @@ final class TermuxInstaller {
|
|||||||
}.start();
|
}.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ensureDirectoryExists(File directory) {
|
|
||||||
if (!directory.isDirectory() && !directory.mkdirs()) {
|
|
||||||
throw new RuntimeException("Unable to create directory: " + directory.getAbsolutePath());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static byte[] loadZipBytes() {
|
|
||||||
// Only load the shared library when necessary to save memory usage.
|
|
||||||
System.loadLibrary("termux-bootstrap");
|
|
||||||
return getZip();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static native byte[] getZip();
|
|
||||||
|
|
||||||
/** Delete a folder and all its content or throw. Don't follow symlinks. */
|
|
||||||
static void deleteFolder(File fileOrDirectory) throws IOException {
|
|
||||||
if (fileOrDirectory.getCanonicalPath().equals(fileOrDirectory.getAbsolutePath()) && fileOrDirectory.isDirectory()) {
|
|
||||||
File[] children = fileOrDirectory.listFiles();
|
|
||||||
|
|
||||||
if (children != null) {
|
|
||||||
for (File child : children) {
|
|
||||||
deleteFolder(child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fileOrDirectory.delete()) {
|
|
||||||
throw new RuntimeException("Unable to delete " + (fileOrDirectory.isDirectory() ? "directory " : "file ") + fileOrDirectory.getAbsolutePath());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void setupStorageSymlinks(final Context context) {
|
static void setupStorageSymlinks(final Context context) {
|
||||||
final String LOG_TAG = "termux-storage";
|
final String LOG_TAG = "termux-storage";
|
||||||
|
|
||||||
|
Logger.logInfo(LOG_TAG, "Setting up storage symlinks.");
|
||||||
|
|
||||||
new Thread() {
|
new Thread() {
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
File storageDir = new File(TermuxService.HOME_PATH, "storage");
|
String errmsg;
|
||||||
|
File storageDir = TermuxConstants.TERMUX_STORAGE_HOME_DIR;
|
||||||
|
|
||||||
if (storageDir.exists()) {
|
errmsg = FileUtils.clearDirectory(context, "~/storage", storageDir.getAbsolutePath());
|
||||||
try {
|
if (errmsg != null) {
|
||||||
deleteFolder(storageDir);
|
Logger.logErrorAndShowToast(context, LOG_TAG, errmsg);
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(LOG_TAG, "Could not delete old $HOME/storage, " + e.getMessage());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!storageDir.mkdirs()) {
|
|
||||||
Log.e(LOG_TAG, "Unable to mkdirs() for $HOME/storage");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/shared, ~/storage/downloads, ~/storage/dcim, ~/storage/pictures, ~/storage/music and ~/storage/movies for directories in \"" + Environment.getExternalStorageDirectory().getAbsolutePath() + "\".");
|
||||||
|
|
||||||
File sharedDir = Environment.getExternalStorageDirectory();
|
File sharedDir = Environment.getExternalStorageDirectory();
|
||||||
Os.symlink(sharedDir.getAbsolutePath(), new File(storageDir, "shared").getAbsolutePath());
|
Os.symlink(sharedDir.getAbsolutePath(), new File(storageDir, "shared").getAbsolutePath());
|
||||||
|
|
||||||
@@ -234,14 +233,34 @@ final class TermuxInstaller {
|
|||||||
File dir = dirs[i];
|
File dir = dirs[i];
|
||||||
if (dir == null) continue;
|
if (dir == null) continue;
|
||||||
String symlinkName = "external-" + i;
|
String symlinkName = "external-" + i;
|
||||||
|
Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/" + symlinkName + " for \"" + dir.getAbsolutePath() + "\".");
|
||||||
Os.symlink(dir.getAbsolutePath(), new File(storageDir, symlinkName).getAbsolutePath());
|
Os.symlink(dir.getAbsolutePath(), new File(storageDir, symlinkName).getAbsolutePath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.logInfo(LOG_TAG, "Storage symlinks created successfully.");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(LOG_TAG, "Error setting up link", e);
|
Logger.logStackTraceWithMessage(LOG_TAG, "Error setting up link", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.start();
|
}.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ensureDirectoryExists(Context context, File directory) {
|
||||||
|
String errmsg;
|
||||||
|
|
||||||
|
errmsg = FileUtils.createDirectoryFile(context, directory.getAbsolutePath());
|
||||||
|
if (errmsg != null) {
|
||||||
|
throw new RuntimeException(errmsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] loadZipBytes() {
|
||||||
|
// Only load the shared library when necessary to save memory usage.
|
||||||
|
System.loadLibrary("termux-bootstrap");
|
||||||
|
return getZip();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static native byte[] getZip();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import android.net.Uri;
|
|||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
import android.util.Log;
|
|
||||||
import android.webkit.MimeTypeMap;
|
import android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
import com.termux.terminal.EmulatorDebug;
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
@@ -24,11 +24,13 @@ import androidx.annotation.NonNull;
|
|||||||
|
|
||||||
public class TermuxOpenReceiver extends BroadcastReceiver {
|
public class TermuxOpenReceiver extends BroadcastReceiver {
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "TermuxOpenReceiver";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onReceive(Context context, Intent intent) {
|
public void onReceive(Context context, Intent intent) {
|
||||||
final Uri data = intent.getData();
|
final Uri data = intent.getData();
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
Log.e(EmulatorDebug.LOG_TAG, "termux-open: Called without intent data");
|
Logger.logError(LOG_TAG, "termux-open: Called without intent data");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +44,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||||||
// Ok.
|
// Ok.
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
Log.e(EmulatorDebug.LOG_TAG, "Invalid action '" + intentAction + "', using 'view'");
|
Logger.logError(LOG_TAG, "Invalid action '" + intentAction + "', using 'view'");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,14 +61,14 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||||||
try {
|
try {
|
||||||
context.startActivity(urlIntent);
|
context.startActivity(urlIntent);
|
||||||
} catch (ActivityNotFoundException e) {
|
} catch (ActivityNotFoundException e) {
|
||||||
Log.e(EmulatorDebug.LOG_TAG, "termux-open: No app handles the url " + data);
|
Logger.logError(LOG_TAG, "termux-open: No app handles the url " + data);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final File fileToShare = new File(filePath);
|
final File fileToShare = new File(filePath);
|
||||||
if (!(fileToShare.isFile() && fileToShare.canRead())) {
|
if (!(fileToShare.isFile() && fileToShare.canRead())) {
|
||||||
Log.e(EmulatorDebug.LOG_TAG, "termux-open: Not a readable file: '" + fileToShare.getAbsolutePath() + "'");
|
Logger.logError(LOG_TAG, "termux-open: Not a readable file: '" + fileToShare.getAbsolutePath() + "'");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +89,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||||||
contentTypeToUse = contentTypeExtra;
|
contentTypeToUse = contentTypeExtra;
|
||||||
}
|
}
|
||||||
|
|
||||||
Uri uriToShare = Uri.parse("content://com.termux.files" + fileToShare.getAbsolutePath());
|
Uri uriToShare = Uri.parse("content://" + TermuxConstants.TERMUX_FILE_SHARE_URI_AUTHORITY + fileToShare.getAbsolutePath());
|
||||||
|
|
||||||
if (Intent.ACTION_SEND.equals(intentAction)) {
|
if (Intent.ACTION_SEND.equals(intentAction)) {
|
||||||
sendIntent.putExtra(Intent.EXTRA_STREAM, uriToShare);
|
sendIntent.putExtra(Intent.EXTRA_STREAM, uriToShare);
|
||||||
@@ -103,7 +105,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||||||
try {
|
try {
|
||||||
context.startActivity(sendIntent);
|
context.startActivity(sendIntent);
|
||||||
} catch (ActivityNotFoundException e) {
|
} catch (ActivityNotFoundException e) {
|
||||||
Log.e(EmulatorDebug.LOG_TAG, "termux-open: No app handles the url " + data);
|
Logger.logError(LOG_TAG, "termux-open: No app handles the url " + data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +180,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||||||
String path = file.getCanonicalPath();
|
String path = file.getCanonicalPath();
|
||||||
String storagePath = Environment.getExternalStorageDirectory().getCanonicalPath();
|
String storagePath = Environment.getExternalStorageDirectory().getCanonicalPath();
|
||||||
// See https://support.google.com/faqs/answer/7496913:
|
// See https://support.google.com/faqs/answer/7496913:
|
||||||
if (!(path.startsWith(TermuxService.FILES_PATH) || path.startsWith(storagePath))) {
|
if (!(path.startsWith(TermuxConstants.TERMUX_FILES_DIR_PATH) || path.startsWith(storagePath))) {
|
||||||
throw new IllegalArgumentException("Invalid path: " + path);
|
throw new IllegalArgumentException("Invalid path: " + path);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|||||||
@@ -1,251 +0,0 @@
|
|||||||
package com.termux.app;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.util.TypedValue;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import com.termux.terminal.TerminalSession;
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.lang.annotation.Retention;
|
|
||||||
import java.lang.annotation.RetentionPolicy;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
import androidx.annotation.IntDef;
|
|
||||||
|
|
||||||
final class TermuxPreferences {
|
|
||||||
|
|
||||||
@IntDef({BELL_VIBRATE, BELL_BEEP, BELL_IGNORE})
|
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
|
||||||
@interface AsciiBellBehaviour {
|
|
||||||
}
|
|
||||||
|
|
||||||
final static class KeyboardShortcut {
|
|
||||||
|
|
||||||
KeyboardShortcut(int codePoint, int shortcutAction) {
|
|
||||||
this.codePoint = codePoint;
|
|
||||||
this.shortcutAction = shortcutAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
final int codePoint;
|
|
||||||
final int shortcutAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
static final int SHORTCUT_ACTION_CREATE_SESSION = 1;
|
|
||||||
static final int SHORTCUT_ACTION_NEXT_SESSION = 2;
|
|
||||||
static final int SHORTCUT_ACTION_PREVIOUS_SESSION = 3;
|
|
||||||
static final int SHORTCUT_ACTION_RENAME_SESSION = 4;
|
|
||||||
|
|
||||||
static final int BELL_VIBRATE = 1;
|
|
||||||
static final int BELL_BEEP = 2;
|
|
||||||
static final int BELL_IGNORE = 3;
|
|
||||||
|
|
||||||
private final int MIN_FONTSIZE;
|
|
||||||
private static final int MAX_FONTSIZE = 256;
|
|
||||||
|
|
||||||
private static final String SHOW_EXTRA_KEYS_KEY = "show_extra_keys";
|
|
||||||
private static final String FONTSIZE_KEY = "fontsize";
|
|
||||||
private static final String CURRENT_SESSION_KEY = "current_session";
|
|
||||||
private static final String SCREEN_ALWAYS_ON_KEY = "screen_always_on";
|
|
||||||
|
|
||||||
private boolean mUseDarkUI;
|
|
||||||
private boolean mScreenAlwaysOn;
|
|
||||||
private int mFontSize;
|
|
||||||
|
|
||||||
@AsciiBellBehaviour
|
|
||||||
int mBellBehaviour = BELL_VIBRATE;
|
|
||||||
|
|
||||||
boolean mBackIsEscape;
|
|
||||||
boolean mDisableVolumeVirtualKeys;
|
|
||||||
boolean mShowExtraKeys;
|
|
||||||
|
|
||||||
ExtraKeysInfos mExtraKeys;
|
|
||||||
|
|
||||||
final List<KeyboardShortcut> shortcuts = new ArrayList<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If value is not in the range [min, max], set it to either min or max.
|
|
||||||
*/
|
|
||||||
static int clamp(int value, int min, int max) {
|
|
||||||
return Math.min(Math.max(value, min), max);
|
|
||||||
}
|
|
||||||
|
|
||||||
TermuxPreferences(Context context) {
|
|
||||||
reloadFromProperties(context);
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
|
||||||
|
|
||||||
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
|
|
||||||
// to prevent invisible text due to zoom be mistake:
|
|
||||||
MIN_FONTSIZE = (int) (4f * dipInPixels);
|
|
||||||
|
|
||||||
mShowExtraKeys = prefs.getBoolean(SHOW_EXTRA_KEYS_KEY, true);
|
|
||||||
mScreenAlwaysOn = prefs.getBoolean(SCREEN_ALWAYS_ON_KEY, false);
|
|
||||||
|
|
||||||
// http://www.google.com/design/spec/style/typography.html#typography-line-height
|
|
||||||
int defaultFontSize = Math.round(12 * dipInPixels);
|
|
||||||
// Make it divisible by 2 since that is the minimal adjustment step:
|
|
||||||
if (defaultFontSize % 2 == 1) defaultFontSize--;
|
|
||||||
|
|
||||||
try {
|
|
||||||
mFontSize = Integer.parseInt(prefs.getString(FONTSIZE_KEY, Integer.toString(defaultFontSize)));
|
|
||||||
} catch (NumberFormatException | ClassCastException e) {
|
|
||||||
mFontSize = defaultFontSize;
|
|
||||||
}
|
|
||||||
mFontSize = clamp(mFontSize, MIN_FONTSIZE, MAX_FONTSIZE);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean toggleShowExtraKeys(Context context) {
|
|
||||||
mShowExtraKeys = !mShowExtraKeys;
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SHOW_EXTRA_KEYS_KEY, mShowExtraKeys).apply();
|
|
||||||
return mShowExtraKeys;
|
|
||||||
}
|
|
||||||
|
|
||||||
int getFontSize() {
|
|
||||||
return mFontSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
void changeFontSize(Context context, boolean increase) {
|
|
||||||
mFontSize += (increase ? 1 : -1) * 2;
|
|
||||||
mFontSize = Math.max(MIN_FONTSIZE, Math.min(mFontSize, MAX_FONTSIZE));
|
|
||||||
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
|
||||||
prefs.edit().putString(FONTSIZE_KEY, Integer.toString(mFontSize)).apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isScreenAlwaysOn() {
|
|
||||||
return mScreenAlwaysOn;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isUsingBlackUI() {
|
|
||||||
return mUseDarkUI;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setScreenAlwaysOn(Context context, boolean newValue) {
|
|
||||||
mScreenAlwaysOn = newValue;
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SCREEN_ALWAYS_ON_KEY, newValue).apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void storeCurrentSession(Context context, TerminalSession session) {
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putString(TermuxPreferences.CURRENT_SESSION_KEY, session.mHandle).apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
static TerminalSession getCurrentSession(TermuxActivity context) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
void reloadFromProperties(Context context) {
|
|
||||||
File propsFile = new File(TermuxService.HOME_PATH + "/.termux/termux.properties");
|
|
||||||
if (!propsFile.exists())
|
|
||||||
propsFile = new File(TermuxService.HOME_PATH + "/.config/termux/termux.properties");
|
|
||||||
|
|
||||||
Properties props = new Properties();
|
|
||||||
try {
|
|
||||||
if (propsFile.isFile() && propsFile.canRead()) {
|
|
||||||
try (FileInputStream in = new FileInputStream(propsFile)) {
|
|
||||||
props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Toast.makeText(context, "Could not open properties file termux.properties: " + e.getMessage(), Toast.LENGTH_LONG).show();
|
|
||||||
Log.e("termux", "Error loading props", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (props.getProperty("bell-character", "vibrate")) {
|
|
||||||
case "beep":
|
|
||||||
mBellBehaviour = BELL_BEEP;
|
|
||||||
break;
|
|
||||||
case "ignore":
|
|
||||||
mBellBehaviour = BELL_IGNORE;
|
|
||||||
break;
|
|
||||||
default: // "vibrate".
|
|
||||||
mBellBehaviour = BELL_VIBRATE;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (props.getProperty("use-black-ui", "").toLowerCase()) {
|
|
||||||
case "true":
|
|
||||||
mUseDarkUI = true;
|
|
||||||
break;
|
|
||||||
case "false":
|
|
||||||
mUseDarkUI = false;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
int nightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
|
||||||
mUseDarkUI = nightMode == Configuration.UI_MODE_NIGHT_YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
String defaultExtraKeys = "[[ESC, TAB, CTRL, ALT, {key: '-', popup: '|'}, DOWN, UP]]";
|
|
||||||
|
|
||||||
try {
|
|
||||||
String extrakeyProp = props.getProperty("extra-keys", defaultExtraKeys);
|
|
||||||
String extraKeysStyle = props.getProperty("extra-keys-style", "default");
|
|
||||||
mExtraKeys = new ExtraKeysInfos(extrakeyProp, extraKeysStyle);
|
|
||||||
} catch (JSONException e) {
|
|
||||||
Toast.makeText(context, "Could not load the extra-keys property from the config: " + e.toString(), Toast.LENGTH_LONG).show();
|
|
||||||
Log.e("termux", "Error loading props", e);
|
|
||||||
|
|
||||||
try {
|
|
||||||
mExtraKeys = new ExtraKeysInfos(defaultExtraKeys, "default");
|
|
||||||
} catch (JSONException e2) {
|
|
||||||
e2.printStackTrace();
|
|
||||||
Toast.makeText(context, "Can't create default extra keys", Toast.LENGTH_LONG).show();
|
|
||||||
mExtraKeys = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mBackIsEscape = "escape".equals(props.getProperty("back-key", "back"));
|
|
||||||
mDisableVolumeVirtualKeys = "volume".equals(props.getProperty("volume-keys", "virtual"));
|
|
||||||
|
|
||||||
shortcuts.clear();
|
|
||||||
parseAction("shortcut.create-session", SHORTCUT_ACTION_CREATE_SESSION, props);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
package com.termux.app;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.media.AudioManager;
|
|
||||||
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;
|
|
||||||
|
|
||||||
import androidx.drawerlayout.widget.DrawerLayout;
|
|
||||||
|
|
||||||
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 == '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.readSpecialButton(ExtraKeysView.SpecialButton.CTRL)) || mVirtualControlKeyDown;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean readAltKey() {
|
|
||||||
return (mActivity.mExtraKeysView != null && mActivity.mExtraKeysView.readSpecialButton(ExtraKeysView.SpecialButton.ALT));
|
|
||||||
}
|
|
||||||
|
|
||||||
@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':
|
|
||||||
case 'k':
|
|
||||||
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 (mActivity.mSettings.mDisableVolumeVirtualKeys) {
|
|
||||||
return false;
|
|
||||||
} else if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
|
|
||||||
// Do not steal dedicated buttons from a full external keyboard.
|
|
||||||
return false;
|
|
||||||
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
|
|
||||||
mVirtualControlKeyDown = down;
|
|
||||||
return true;
|
|
||||||
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
|
||||||
mVirtualFnKeyDown = down;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.termux.app;
|
package com.termux.app.activities;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.ActivityNotFoundException;
|
import android.content.ActivityNotFoundException;
|
||||||
@@ -13,7 +13,7 @@ import android.widget.ProgressBar;
|
|||||||
import android.widget.RelativeLayout;
|
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 HelpActivity extends Activity {
|
||||||
|
|
||||||
WebView mWebView;
|
WebView mWebView;
|
||||||
|
|
||||||
180
app/src/main/java/com/termux/app/activities/ReportActivity.java
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
package com.termux.app.activities;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
import com.termux.shared.markdown.MarkdownUtils;
|
||||||
|
import com.termux.shared.interact.ShareUtils;
|
||||||
|
import com.termux.app.models.ReportInfo;
|
||||||
|
|
||||||
|
import org.commonmark.node.FencedCodeBlock;
|
||||||
|
|
||||||
|
import io.noties.markwon.Markwon;
|
||||||
|
import io.noties.markwon.recycler.MarkwonAdapter;
|
||||||
|
import io.noties.markwon.recycler.SimpleEntry;
|
||||||
|
|
||||||
|
public class ReportActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private static final String EXTRA_REPORT_INFO = "report_info";
|
||||||
|
|
||||||
|
ReportInfo mReportInfo;
|
||||||
|
String mReportMarkdownString;
|
||||||
|
String mReportActivityMarkdownString;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_report);
|
||||||
|
|
||||||
|
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
|
if (toolbar != null) {
|
||||||
|
setSupportActionBar(toolbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bundle bundle = null;
|
||||||
|
Intent intent = getIntent();
|
||||||
|
if (intent != null)
|
||||||
|
bundle = intent.getExtras();
|
||||||
|
else if (savedInstanceState != null)
|
||||||
|
bundle = savedInstanceState;
|
||||||
|
|
||||||
|
updateUI(bundle);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onNewIntent(Intent intent) {
|
||||||
|
super.onNewIntent(intent);
|
||||||
|
setIntent(intent);
|
||||||
|
|
||||||
|
if (intent != null)
|
||||||
|
updateUI(intent.getExtras());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateUI(Bundle bundle) {
|
||||||
|
|
||||||
|
if (bundle == null) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mReportInfo = (ReportInfo) bundle.getSerializable(EXTRA_REPORT_INFO);
|
||||||
|
|
||||||
|
if (mReportInfo == null) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
final ActionBar actionBar = getSupportActionBar();
|
||||||
|
if (actionBar != null) {
|
||||||
|
if (mReportInfo.reportTitle != null)
|
||||||
|
actionBar.setTitle(mReportInfo.reportTitle);
|
||||||
|
else
|
||||||
|
actionBar.setTitle(TermuxConstants.TERMUX_APP_NAME + " App Report");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
RecyclerView recyclerView = findViewById(R.id.recycler_view);
|
||||||
|
|
||||||
|
final Markwon markwon = MarkdownUtils.getRecyclerMarkwonBuilder(this);
|
||||||
|
|
||||||
|
final MarkwonAdapter adapter = MarkwonAdapter.builderTextViewIsRoot(R.layout.activity_report_adapter_node_default)
|
||||||
|
.include(FencedCodeBlock.class, SimpleEntry.create(R.layout.activity_report_adapter_node_code_block, R.id.code_text_view))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||||
|
recyclerView.setAdapter(adapter);
|
||||||
|
|
||||||
|
|
||||||
|
generateReportActivityMarkdownString();
|
||||||
|
adapter.setMarkdown(markwon, mReportActivityMarkdownString);
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||||
|
super.onSaveInstanceState(outState);
|
||||||
|
|
||||||
|
outState.putSerializable(EXTRA_REPORT_INFO, mReportInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||||
|
final MenuInflater inflater = getMenuInflater();
|
||||||
|
inflater.inflate(R.menu.menu_report, menu);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
// Remove activity from recents menu on back button press
|
||||||
|
finishAndRemoveTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
|
int id = item.getItemId();
|
||||||
|
if (id == R.id.menu_item_share_report) {
|
||||||
|
if (mReportMarkdownString != null)
|
||||||
|
ShareUtils.shareText(this, getString(R.string.title_report_text), mReportMarkdownString);
|
||||||
|
} else if (id == R.id.menu_item_copy_report) {
|
||||||
|
if (mReportMarkdownString != null)
|
||||||
|
ShareUtils.copyTextToClipboard(this, mReportMarkdownString, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the markdown {@link String} to be shown in {@link ReportActivity}.
|
||||||
|
*/
|
||||||
|
private void generateReportActivityMarkdownString() {
|
||||||
|
mReportMarkdownString = ReportInfo.getReportInfoMarkdownString(mReportInfo);
|
||||||
|
|
||||||
|
mReportActivityMarkdownString = "";
|
||||||
|
if (mReportInfo.reportStringPrefix != null)
|
||||||
|
mReportActivityMarkdownString += mReportInfo.reportStringPrefix;
|
||||||
|
|
||||||
|
mReportActivityMarkdownString += mReportMarkdownString;
|
||||||
|
|
||||||
|
if (mReportInfo.reportStringSuffix != null)
|
||||||
|
mReportActivityMarkdownString += mReportInfo.reportStringSuffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static void startReportActivity(@NonNull final Context context, @NonNull final ReportInfo reportInfo) {
|
||||||
|
context.startActivity(newInstance(context, reportInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Intent newInstance(@NonNull final Context context, @NonNull final ReportInfo reportInfo) {
|
||||||
|
Intent intent = new Intent(context, ReportActivity.class);
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putSerializable(EXTRA_REPORT_INFO, reportInfo);
|
||||||
|
intent.putExtras(bundle);
|
||||||
|
|
||||||
|
// Note that ReportActivity task has documentLaunchMode="intoExisting" set in AndroidManifest.xml
|
||||||
|
// which has equivalent behaviour to the following. The following dynamic way doesn't seem to
|
||||||
|
// work for notification pending intent, i.e separate task isn't created and activity is
|
||||||
|
// launched in the same task as TermuxActivity.
|
||||||
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.termux.app.activities;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
|
||||||
|
public class SettingsActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_settings);
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
getSupportFragmentManager()
|
||||||
|
.beginTransaction()
|
||||||
|
.replace(R.id.settings, new RootPreferencesFragment())
|
||||||
|
.commit();
|
||||||
|
}
|
||||||
|
ActionBar actionBar = getSupportActionBar();
|
||||||
|
if (actionBar != null) {
|
||||||
|
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||||
|
actionBar.setDisplayShowHomeEnabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onSupportNavigateUp() {
|
||||||
|
onBackPressed();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class RootPreferencesFragment extends PreferenceFragmentCompat {
|
||||||
|
@Override
|
||||||
|
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||||
|
setPreferencesFromResource(R.xml.root_preferences, rootKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package com.termux.app.fragments.settings;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.annotation.Keep;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.preference.ListPreference;
|
||||||
|
import androidx.preference.PreferenceCategory;
|
||||||
|
import androidx.preference.PreferenceDataStore;
|
||||||
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||||
|
PreferenceManager preferenceManager = getPreferenceManager();
|
||||||
|
preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(getContext()));
|
||||||
|
|
||||||
|
setPreferencesFromResource(R.xml.debugging_preferences, rootKey);
|
||||||
|
|
||||||
|
PreferenceCategory loggingCategory = findPreference("logging");
|
||||||
|
|
||||||
|
if (loggingCategory != null) {
|
||||||
|
final ListPreference logLevelListPreference = setLogLevelListPreferenceData(findPreference("log_level"), getActivity());
|
||||||
|
loggingCategory.addPreference(logLevelListPreference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ListPreference setLogLevelListPreferenceData(ListPreference logLevelListPreference, Context context) {
|
||||||
|
if (logLevelListPreference == null)
|
||||||
|
logLevelListPreference = new ListPreference(context);
|
||||||
|
|
||||||
|
CharSequence[] logLevels = Logger.getLogLevelsArray();
|
||||||
|
CharSequence[] logLevelLabels = Logger.getLogLevelLabelsArray(context, logLevels, true);
|
||||||
|
|
||||||
|
logLevelListPreference.setEntryValues(logLevels);
|
||||||
|
logLevelListPreference.setEntries(logLevelLabels);
|
||||||
|
|
||||||
|
logLevelListPreference.setValue(String.valueOf(Logger.getLogLevel()));
|
||||||
|
logLevelListPreference.setDefaultValue(Logger.getLogLevel());
|
||||||
|
|
||||||
|
return logLevelListPreference;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class DebuggingPreferencesDataStore extends PreferenceDataStore {
|
||||||
|
|
||||||
|
private final Context mContext;
|
||||||
|
private final TermuxAppSharedPreferences mPreferences;
|
||||||
|
|
||||||
|
private static DebuggingPreferencesDataStore mInstance;
|
||||||
|
|
||||||
|
private DebuggingPreferencesDataStore(Context context) {
|
||||||
|
mContext = context;
|
||||||
|
mPreferences = new TermuxAppSharedPreferences(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized DebuggingPreferencesDataStore getInstance(Context context) {
|
||||||
|
if (mInstance == null) {
|
||||||
|
mInstance = new DebuggingPreferencesDataStore(context.getApplicationContext());
|
||||||
|
}
|
||||||
|
return mInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public String getString(String key, @Nullable String defValue) {
|
||||||
|
if (key == null) return null;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case "log_level":
|
||||||
|
return String.valueOf(mPreferences.getLogLevel());
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void putString(String key, @Nullable String value) {
|
||||||
|
if (key == null) return;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case "log_level":
|
||||||
|
if (value != null) {
|
||||||
|
mPreferences.setLogLevel(mContext, Integer.parseInt(value));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void putBoolean(String key, boolean value) {
|
||||||
|
if (key == null) return;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case "terminal_view_key_logging_enabled":
|
||||||
|
mPreferences.setTerminalViewKeyLoggingEnabled(value);
|
||||||
|
break;
|
||||||
|
case "plugin_error_notifications_enabled":
|
||||||
|
mPreferences.setPluginErrorNotificationsEnabled(value);
|
||||||
|
break;
|
||||||
|
case "crash_report_notifications_enabled":
|
||||||
|
mPreferences.setCrashReportNotificationsEnabled(value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getBoolean(String key, boolean defValue) {
|
||||||
|
switch (key) {
|
||||||
|
case "terminal_view_key_logging_enabled":
|
||||||
|
return mPreferences.getTerminalViewKeyLoggingEnabled();
|
||||||
|
case "plugin_error_notifications_enabled":
|
||||||
|
return mPreferences.getPluginErrorNotificationsEnabled();
|
||||||
|
case "crash_report_notifications_enabled":
|
||||||
|
return mPreferences.getCrashReportNotificationsEnabled();
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package com.termux.app.fragments.settings;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.annotation.Keep;
|
||||||
|
import androidx.preference.PreferenceDataStore;
|
||||||
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
public class TerminalIOPreferencesFragment extends PreferenceFragmentCompat {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||||
|
PreferenceManager preferenceManager = getPreferenceManager();
|
||||||
|
preferenceManager.setPreferenceDataStore(TerminalIOPreferencesDataStore.getInstance(getContext()));
|
||||||
|
|
||||||
|
setPreferencesFromResource(R.xml.terminal_io_preferences, rootKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class TerminalIOPreferencesDataStore extends PreferenceDataStore {
|
||||||
|
|
||||||
|
private final Context mContext;
|
||||||
|
private final TermuxAppSharedPreferences mPreferences;
|
||||||
|
|
||||||
|
private static TerminalIOPreferencesDataStore mInstance;
|
||||||
|
|
||||||
|
private TerminalIOPreferencesDataStore(Context context) {
|
||||||
|
mContext = context;
|
||||||
|
mPreferences = new TermuxAppSharedPreferences(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized TerminalIOPreferencesDataStore getInstance(Context context) {
|
||||||
|
if (mInstance == null) {
|
||||||
|
mInstance = new TerminalIOPreferencesDataStore(context.getApplicationContext());
|
||||||
|
}
|
||||||
|
return mInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void putBoolean(String key, boolean value) {
|
||||||
|
if (key == null) return;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case "soft_keyboard_enabled":
|
||||||
|
mPreferences.setSoftKeyboardEnabled(value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getBoolean(String key, boolean defValue) {
|
||||||
|
switch (key) {
|
||||||
|
case "soft_keyboard_enabled":
|
||||||
|
return mPreferences.getSoftKeyboardEnabled();
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
64
app/src/main/java/com/termux/app/models/ReportInfo.java
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package com.termux.app.models;
|
||||||
|
|
||||||
|
import com.termux.shared.markdown.MarkdownUtils;
|
||||||
|
import com.termux.shared.termux.TermuxUtils;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
public class ReportInfo implements Serializable {
|
||||||
|
|
||||||
|
/** The user action that was being processed for which the report was generated. */
|
||||||
|
public final UserAction userAction;
|
||||||
|
/** The internal app component that sent the report. */
|
||||||
|
public final String sender;
|
||||||
|
/** The report title. */
|
||||||
|
public final String reportTitle;
|
||||||
|
/** The markdown report text prefix. Will not be part of copy and share operations, etc. */
|
||||||
|
public final String reportStringPrefix;
|
||||||
|
/** The markdown report text. */
|
||||||
|
public final String reportString;
|
||||||
|
/** The markdown report text suffix. Will not be part of copy and share operations, etc. */
|
||||||
|
public final String reportStringSuffix;
|
||||||
|
/** If set to {@code true}, then report, app and device info will be added to the report when
|
||||||
|
* markdown is generated.
|
||||||
|
*/
|
||||||
|
public final boolean addReportInfoToMarkdown;
|
||||||
|
/** The timestamp for the report. */
|
||||||
|
public final String reportTimestamp;
|
||||||
|
|
||||||
|
public ReportInfo(UserAction userAction, String sender, String reportTitle, String reportStringPrefix, String reportString, String reportStringSuffix, boolean addReportInfoToMarkdown) {
|
||||||
|
this.userAction = userAction;
|
||||||
|
this.sender = sender;
|
||||||
|
this.reportTitle = reportTitle;
|
||||||
|
this.reportStringPrefix = reportStringPrefix;
|
||||||
|
this.reportString = reportString;
|
||||||
|
this.reportStringSuffix = reportStringSuffix;
|
||||||
|
this.addReportInfoToMarkdown = addReportInfoToMarkdown;
|
||||||
|
this.reportTimestamp = TermuxUtils.getCurrentTimeStamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a markdown {@link String} for {@link ReportInfo}.
|
||||||
|
*
|
||||||
|
* @param reportInfo The {@link ReportInfo} to convert.
|
||||||
|
* @return Returns the markdown {@link String}.
|
||||||
|
*/
|
||||||
|
public static String getReportInfoMarkdownString(final ReportInfo reportInfo) {
|
||||||
|
if (reportInfo == null) return "null";
|
||||||
|
|
||||||
|
StringBuilder markdownString = new StringBuilder();
|
||||||
|
|
||||||
|
if (reportInfo.addReportInfoToMarkdown) {
|
||||||
|
markdownString.append("## Report Info\n\n");
|
||||||
|
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("User Action", reportInfo.userAction, "-"));
|
||||||
|
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Sender", reportInfo.sender, "-"));
|
||||||
|
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Report Timestamp", reportInfo.reportTimestamp, "-"));
|
||||||
|
markdownString.append("\n##\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
markdownString.append(reportInfo.reportString);
|
||||||
|
|
||||||
|
return markdownString.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
19
app/src/main/java/com/termux/app/models/UserAction.java
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package com.termux.app.models;
|
||||||
|
|
||||||
|
public enum UserAction {
|
||||||
|
|
||||||
|
PLUGIN_EXECUTION_COMMAND("plugin execution command"),
|
||||||
|
CRASH_REPORT("crash report"),
|
||||||
|
REPORT_ISSUE_FROM_TRANSCRIPT("report issue from transcript");
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
UserAction(final String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package com.termux.app.settings.properties;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import com.termux.app.terminal.io.KeyboardShortcut;
|
||||||
|
import com.termux.app.terminal.io.extrakeys.ExtraKeysInfo;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.settings.properties.SharedPropertiesParser;
|
||||||
|
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||||
|
import com.termux.shared.settings.properties.TermuxSharedProperties;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public class TermuxAppSharedProperties extends TermuxSharedProperties implements SharedPropertiesParser {
|
||||||
|
|
||||||
|
private ExtraKeysInfo mExtraKeysInfo;
|
||||||
|
private List<KeyboardShortcut> mSessionShortcuts = new ArrayList<>();
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "TermuxAppSharedProperties";
|
||||||
|
|
||||||
|
public TermuxAppSharedProperties(@Nonnull Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload the termux properties from disk into an in-memory cache.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void loadTermuxPropertiesFromDisk() {
|
||||||
|
super.loadTermuxPropertiesFromDisk();
|
||||||
|
|
||||||
|
setExtraKeys();
|
||||||
|
setSessionShortcuts();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the terminal extra keys and style.
|
||||||
|
*/
|
||||||
|
private void setExtraKeys() {
|
||||||
|
mExtraKeysInfo = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// The mMap stores the extra key and style string values while loading properties
|
||||||
|
// Check {@link #getExtraKeysInternalPropertyValueFromValue(String)} and
|
||||||
|
// {@link #getExtraKeysStyleInternalPropertyValueFromValue(String)}
|
||||||
|
String extrakeys = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS, true);
|
||||||
|
String extraKeysStyle = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE, true);
|
||||||
|
mExtraKeysInfo = new ExtraKeysInfo(extrakeys, extraKeysStyle);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
Logger.showToast(mContext, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: " + e.toString(), true);
|
||||||
|
Logger.logStackTraceWithMessage(LOG_TAG, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: ", e);
|
||||||
|
|
||||||
|
try {
|
||||||
|
mExtraKeysInfo = new ExtraKeysInfo(TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE);
|
||||||
|
} catch (JSONException e2) {
|
||||||
|
Logger.showToast(mContext, "Can't create default extra keys",true);
|
||||||
|
Logger.logStackTraceWithMessage(LOG_TAG, "Could create default extra keys: ", e);
|
||||||
|
mExtraKeysInfo = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the terminal sessions shortcuts.
|
||||||
|
*/
|
||||||
|
private void setSessionShortcuts() {
|
||||||
|
if (mSessionShortcuts == null)
|
||||||
|
mSessionShortcuts = new ArrayList<>();
|
||||||
|
else
|
||||||
|
mSessionShortcuts.clear();
|
||||||
|
|
||||||
|
// The {@link TermuxPropertyConstants#MAP_SESSION_SHORTCUTS} stores the session shortcut key and action pair
|
||||||
|
for (Map.Entry<String, Integer> entry : TermuxPropertyConstants.MAP_SESSION_SHORTCUTS.entrySet()) {
|
||||||
|
// The mMap stores the code points for the session shortcuts while loading properties
|
||||||
|
Integer codePoint = (Integer) getInternalPropertyValue(entry.getKey(), true);
|
||||||
|
// If codePoint is null, then session shortcut did not exist in properties or was invalid
|
||||||
|
// as parsed by {@link #getCodePointForSessionShortcuts(String,String)}
|
||||||
|
// If codePoint is not null, then get the action for the MAP_SESSION_SHORTCUTS key and
|
||||||
|
// add the code point to sessionShortcuts
|
||||||
|
if (codePoint != null)
|
||||||
|
mSessionShortcuts.add(new KeyboardShortcut(codePoint, entry.getValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<KeyboardShortcut> getSessionShortcuts() {
|
||||||
|
return mSessionShortcuts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtraKeysInfo getExtraKeysInfo() {
|
||||||
|
return mExtraKeysInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package com.termux.app.terminal;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.text.SpannableString;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.style.StyleSpan;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.AdapterView;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.app.TermuxActivity;
|
||||||
|
import com.termux.shared.shell.TermuxSession;
|
||||||
|
import com.termux.terminal.TerminalSession;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class TermuxSessionsListViewController extends ArrayAdapter<TermuxSession> implements AdapterView.OnItemClickListener, AdapterView.OnItemLongClickListener {
|
||||||
|
|
||||||
|
final TermuxActivity mActivity;
|
||||||
|
|
||||||
|
final StyleSpan boldSpan = new StyleSpan(Typeface.BOLD);
|
||||||
|
final StyleSpan italicSpan = new StyleSpan(Typeface.ITALIC);
|
||||||
|
|
||||||
|
public TermuxSessionsListViewController(TermuxActivity activity, List<TermuxSession> sessionList) {
|
||||||
|
super(activity.getApplicationContext(), R.layout.item_terminal_sessions_list, sessionList);
|
||||||
|
this.mActivity = activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
|
||||||
|
View sessionRowView = convertView;
|
||||||
|
if (sessionRowView == null) {
|
||||||
|
LayoutInflater inflater = mActivity.getLayoutInflater();
|
||||||
|
sessionRowView = inflater.inflate(R.layout.item_terminal_sessions_list, parent, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextView sessionTitleView = sessionRowView.findViewById(R.id.session_title);
|
||||||
|
|
||||||
|
TerminalSession sessionAtRow = getItem(position).getTerminalSession();
|
||||||
|
if (sessionAtRow == null) {
|
||||||
|
sessionTitleView.setText("null session");
|
||||||
|
return sessionRowView;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isUsingBlackUI = mActivity.getProperties().isUsingBlackUI();
|
||||||
|
|
||||||
|
if (isUsingBlackUI) {
|
||||||
|
sessionTitleView.setBackground(
|
||||||
|
ContextCompat.getDrawable(mActivity, R.drawable.session_background_black_selected)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String name = sessionAtRow.mSessionName;
|
||||||
|
String sessionTitle = sessionAtRow.getTitle();
|
||||||
|
|
||||||
|
String numberPart = "[" + (position + 1) + "] ";
|
||||||
|
String sessionNamePart = (TextUtils.isEmpty(name) ? "" : name);
|
||||||
|
String sessionTitlePart = (TextUtils.isEmpty(sessionTitle) ? "" : ((sessionNamePart.isEmpty() ? "" : "\n") + sessionTitle));
|
||||||
|
|
||||||
|
String fullSessionTitle = numberPart + sessionNamePart + sessionTitlePart;
|
||||||
|
SpannableString fullSessionTitleStyled = new SpannableString(fullSessionTitle);
|
||||||
|
fullSessionTitleStyled.setSpan(boldSpan, 0, numberPart.length() + sessionNamePart.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
fullSessionTitleStyled.setSpan(italicSpan, numberPart.length() + sessionNamePart.length(), fullSessionTitle.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
|
||||||
|
sessionTitleView.setText(fullSessionTitleStyled);
|
||||||
|
|
||||||
|
boolean sessionRunning = sessionAtRow.isRunning();
|
||||||
|
|
||||||
|
if (sessionRunning) {
|
||||||
|
sessionTitleView.setPaintFlags(sessionTitleView.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
|
||||||
|
} else {
|
||||||
|
sessionTitleView.setPaintFlags(sessionTitleView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
|
||||||
|
}
|
||||||
|
int defaultColor = isUsingBlackUI ? Color.WHITE : Color.BLACK;
|
||||||
|
int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? defaultColor : Color.RED;
|
||||||
|
sessionTitleView.setTextColor(color);
|
||||||
|
return sessionRowView;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||||
|
TermuxSession clickedSession = getItem(position);
|
||||||
|
mActivity.getTermuxTerminalSessionClient().setCurrentSession(clickedSession.getTerminalSession());
|
||||||
|
mActivity.getDrawer().closeDrawers();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
|
||||||
|
final TermuxSession selectedSession = getItem(position);
|
||||||
|
mActivity.getTermuxTerminalSessionClient().renameSession(selectedSession.getTerminalSession());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,343 @@
|
|||||||
|
package com.termux.app.terminal;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.ClipboardManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.media.AudioAttributes;
|
||||||
|
import android.media.SoundPool;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.widget.ListView;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.shared.shell.TermuxSession;
|
||||||
|
import com.termux.shared.interact.DialogUtils;
|
||||||
|
import com.termux.app.TermuxActivity;
|
||||||
|
import com.termux.shared.terminal.TermuxTerminalSessionClientBase;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
import com.termux.app.TermuxService;
|
||||||
|
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||||
|
import com.termux.app.terminal.io.BellHandler;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.terminal.TerminalColors;
|
||||||
|
import com.termux.terminal.TerminalSession;
|
||||||
|
import com.termux.terminal.TextStyle;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase {
|
||||||
|
|
||||||
|
private final TermuxActivity mActivity;
|
||||||
|
|
||||||
|
private static final int MAX_SESSIONS = 8;
|
||||||
|
|
||||||
|
private final SoundPool mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
|
||||||
|
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||||
|
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
|
||||||
|
|
||||||
|
private final int mBellSoundId;
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "TermuxTerminalSessionClient";
|
||||||
|
|
||||||
|
public TermuxTerminalSessionClient(TermuxActivity activity) {
|
||||||
|
this.mActivity = activity;
|
||||||
|
|
||||||
|
mBellSoundId = mBellSoundPool.load(activity, R.raw.bell, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(TerminalSession changedSession) {
|
||||||
|
if (!mActivity.isVisible()) return;
|
||||||
|
|
||||||
|
if (mActivity.getCurrentSession() == changedSession) mActivity.getTerminalView().onScreenUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTitleChanged(TerminalSession updatedSession) {
|
||||||
|
if (!mActivity.isVisible()) return;
|
||||||
|
|
||||||
|
if (updatedSession != mActivity.getCurrentSession()) {
|
||||||
|
// Only show toast for other sessions than the current one, since the user
|
||||||
|
// probably consciously caused the title change to change in the current session
|
||||||
|
// and don't want an annoying toast for that.
|
||||||
|
mActivity.showToast(toToastTitle(updatedSession), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
termuxSessionListNotifyUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSessionFinished(final TerminalSession finishedSession) {
|
||||||
|
if (mActivity.getTermuxService().wantsToStop()) {
|
||||||
|
// The service wants to stop as soon as possible.
|
||||||
|
mActivity.finishActivityIfNotFinishing();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mActivity.isVisible() && finishedSession != mActivity.getCurrentSession()) {
|
||||||
|
// Show toast for non-current sessions that exit.
|
||||||
|
int indexOfSession = mActivity.getTermuxService().getIndexOfSession(finishedSession);
|
||||||
|
// Verify that session was not removed before we got told about it finishing:
|
||||||
|
if (indexOfSession >= 0)
|
||||||
|
mActivity.showToast(toToastTitle(finishedSession) + " - exited", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mActivity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
|
||||||
|
// On Android TV devices we need to use older behaviour because we may
|
||||||
|
// not be able to have multiple launcher icons.
|
||||||
|
if (mActivity.getTermuxService().getTermuxSessionsSize() > 1) {
|
||||||
|
removeFinishedSession(finishedSession);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Once we have a separate launcher icon for the failsafe session, it
|
||||||
|
// should be safe to auto-close session on exit code '0' or '130'.
|
||||||
|
if (finishedSession.getExitStatus() == 0 || finishedSession.getExitStatus() == 130) {
|
||||||
|
removeFinishedSession(finishedSession);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClipboardText(TerminalSession session, String text) {
|
||||||
|
if (!mActivity.isVisible()) return;
|
||||||
|
|
||||||
|
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
|
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(text)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBell(TerminalSession session) {
|
||||||
|
if (!mActivity.isVisible()) return;
|
||||||
|
|
||||||
|
switch (mActivity.getProperties().getBellBehaviour()) {
|
||||||
|
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_VIBRATE:
|
||||||
|
BellHandler.getInstance(mActivity).doBell();
|
||||||
|
break;
|
||||||
|
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_BEEP:
|
||||||
|
mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
|
||||||
|
break;
|
||||||
|
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_IGNORE:
|
||||||
|
// Ignore the bell character.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onColorsChanged(TerminalSession changedSession) {
|
||||||
|
if (mActivity.getCurrentSession() == changedSession)
|
||||||
|
updateBackgroundColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** Try switching to session. */
|
||||||
|
public void setCurrentSession(TerminalSession session) {
|
||||||
|
if (session == null) return;
|
||||||
|
|
||||||
|
if (mActivity.getTerminalView().attachSession(session)) {
|
||||||
|
// notify about switched session if not already displaying the session
|
||||||
|
notifyOfSessionChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We call the following even when the session is already being displayed since config may
|
||||||
|
// be stale, like current session not selected or scrolled to.
|
||||||
|
checkAndScrollToSession(session);
|
||||||
|
updateBackgroundColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
void notifyOfSessionChange() {
|
||||||
|
if (!mActivity.isVisible()) return;
|
||||||
|
|
||||||
|
TerminalSession session = mActivity.getCurrentSession();
|
||||||
|
mActivity.showToast(toToastTitle(session), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void switchToSession(boolean forward) {
|
||||||
|
TermuxService service = mActivity.getTermuxService();
|
||||||
|
|
||||||
|
TerminalSession currentTerminalSession = mActivity.getCurrentSession();
|
||||||
|
int index = service.getIndexOfSession(currentTerminalSession);
|
||||||
|
int size = service.getTermuxSessionsSize();
|
||||||
|
if (forward) {
|
||||||
|
if (++index >= size) index = 0;
|
||||||
|
} else {
|
||||||
|
if (--index < 0) index = size - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
TermuxSession termuxSession = service.getTermuxSession(index);
|
||||||
|
if (termuxSession != null)
|
||||||
|
setCurrentSession(termuxSession.getTerminalSession());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void switchToSession(int index) {
|
||||||
|
TermuxSession termuxSession = mActivity.getTermuxService().getTermuxSession(index);
|
||||||
|
if (termuxSession != null)
|
||||||
|
setCurrentSession(termuxSession.getTerminalSession());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InflateParams")
|
||||||
|
public void renameSession(final TerminalSession sessionToRename) {
|
||||||
|
if (sessionToRename == null) return;
|
||||||
|
|
||||||
|
DialogUtils.textInput(mActivity, R.string.title_rename_session, sessionToRename.mSessionName, R.string.action_rename_session_confirm, text -> {
|
||||||
|
sessionToRename.mSessionName = text;
|
||||||
|
termuxSessionListNotifyUpdated();
|
||||||
|
}, -1, null, -1, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addNewSession(boolean isFailSafe, String sessionName) {
|
||||||
|
if (mActivity.getTermuxService().getTermuxSessionsSize() >= MAX_SESSIONS) {
|
||||||
|
new AlertDialog.Builder(mActivity).setTitle(R.string.title_max_terminals_reached).setMessage(R.string.msg_max_terminals_reached)
|
||||||
|
.setPositiveButton(android.R.string.ok, null).show();
|
||||||
|
} else {
|
||||||
|
TerminalSession currentSession = mActivity.getCurrentSession();
|
||||||
|
|
||||||
|
String workingDirectory;
|
||||||
|
if (currentSession == null) {
|
||||||
|
workingDirectory = mActivity.getProperties().getDefaultWorkingDirectory();
|
||||||
|
} else {
|
||||||
|
workingDirectory = currentSession.getCwd();
|
||||||
|
}
|
||||||
|
|
||||||
|
TermuxSession newTermuxSession = mActivity.getTermuxService().createTermuxSession(null, null, null, workingDirectory, isFailSafe, sessionName);
|
||||||
|
if (newTermuxSession == null) return;
|
||||||
|
|
||||||
|
TerminalSession newTerminalSession = newTermuxSession.getTerminalSession();
|
||||||
|
setCurrentSession(newTerminalSession);
|
||||||
|
|
||||||
|
mActivity.getDrawer().closeDrawers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurrentStoredSession() {
|
||||||
|
TerminalSession currentSession = mActivity.getCurrentSession();
|
||||||
|
if (currentSession != null)
|
||||||
|
mActivity.getPreferences().setCurrentSession(currentSession.mHandle);
|
||||||
|
else
|
||||||
|
mActivity.getPreferences().setCurrentSession(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The current session as stored or the last one if that does not exist. */
|
||||||
|
public TerminalSession getCurrentStoredSessionOrLast() {
|
||||||
|
TerminalSession stored = getCurrentStoredSession(mActivity);
|
||||||
|
|
||||||
|
if (stored != null) {
|
||||||
|
// If a stored session is in the list of currently running sessions, then return it
|
||||||
|
return stored;
|
||||||
|
} else {
|
||||||
|
// Else return the last session currently running
|
||||||
|
TermuxSession termuxSession = mActivity.getTermuxService().getLastTermuxSession();
|
||||||
|
if (termuxSession != null)
|
||||||
|
return termuxSession.getTerminalSession();
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TerminalSession getCurrentStoredSession(TermuxActivity context) {
|
||||||
|
String sessionHandle = mActivity.getPreferences().getCurrentSession();
|
||||||
|
|
||||||
|
// If no session is stored in shared preferences
|
||||||
|
if (sessionHandle == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Check if the session handle found matches one of the currently running sessions
|
||||||
|
return context.getTermuxService().getTerminalSessionForHandle(sessionHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeFinishedSession(TerminalSession finishedSession) {
|
||||||
|
// Return pressed with finished session - remove it.
|
||||||
|
TermuxService service = mActivity.getTermuxService();
|
||||||
|
|
||||||
|
int index = service.removeTermuxSession(finishedSession);
|
||||||
|
|
||||||
|
int size = mActivity.getTermuxService().getTermuxSessionsSize();
|
||||||
|
if (size == 0) {
|
||||||
|
// There are no sessions to show, so finish the activity.
|
||||||
|
mActivity.finishActivityIfNotFinishing();
|
||||||
|
} else {
|
||||||
|
if (index >= size) {
|
||||||
|
index = size - 1;
|
||||||
|
}
|
||||||
|
TermuxSession termuxSession = service.getTermuxSession(index);
|
||||||
|
if (termuxSession != null)
|
||||||
|
setCurrentSession(termuxSession.getTerminalSession());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void termuxSessionListNotifyUpdated() {
|
||||||
|
mActivity.termuxSessionListNotifyUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void checkAndScrollToSession(TerminalSession session) {
|
||||||
|
if (!mActivity.isVisible()) return;
|
||||||
|
final int indexOfSession = mActivity.getTermuxService().getIndexOfSession(session);
|
||||||
|
if (indexOfSession < 0) return;
|
||||||
|
final ListView termuxSessionsListView = mActivity.findViewById(R.id.terminal_sessions_list);
|
||||||
|
if (termuxSessionsListView == null) return;
|
||||||
|
|
||||||
|
termuxSessionsListView.setItemChecked(indexOfSession, true);
|
||||||
|
// Delay is necessary otherwise sometimes scroll to newly added session does not happen
|
||||||
|
termuxSessionsListView.postDelayed(() -> termuxSessionsListView.smoothScrollToPosition(indexOfSession), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
String toToastTitle(TerminalSession session) {
|
||||||
|
final int indexOfSession = mActivity.getTermuxService().getIndexOfSession(session);
|
||||||
|
if (indexOfSession < 0) return null;
|
||||||
|
StringBuilder toastTitle = new StringBuilder("[" + (indexOfSession + 1) + "]");
|
||||||
|
if (!TextUtils.isEmpty(session.mSessionName)) {
|
||||||
|
toastTitle.append(" ").append(session.mSessionName);
|
||||||
|
}
|
||||||
|
String title = session.getTitle();
|
||||||
|
if (!TextUtils.isEmpty(title)) {
|
||||||
|
// Space to "[${NR}] or newline after session name:
|
||||||
|
toastTitle.append(session.mSessionName == null ? " " : "\n");
|
||||||
|
toastTitle.append(title);
|
||||||
|
}
|
||||||
|
return toastTitle.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void checkForFontAndColors() {
|
||||||
|
try {
|
||||||
|
File colorsFile = TermuxConstants.TERMUX_COLOR_PROPERTIES_FILE;
|
||||||
|
File fontFile = TermuxConstants.TERMUX_FONT_FILE;
|
||||||
|
|
||||||
|
final Properties props = new Properties();
|
||||||
|
if (colorsFile.isFile()) {
|
||||||
|
try (InputStream in = new FileInputStream(colorsFile)) {
|
||||||
|
props.load(in);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TerminalColors.COLOR_SCHEME.updateWith(props);
|
||||||
|
TerminalSession session = mActivity.getCurrentSession();
|
||||||
|
if (session != null && session.getEmulator() != null) {
|
||||||
|
session.getEmulator().mColors.reset();
|
||||||
|
}
|
||||||
|
updateBackgroundColor();
|
||||||
|
|
||||||
|
final Typeface newTypeface = (fontFile.exists() && fontFile.length() > 0) ? Typeface.createFromFile(fontFile) : Typeface.MONOSPACE;
|
||||||
|
mActivity.getTerminalView().setTypeface(newTypeface);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.logStackTraceWithMessage(LOG_TAG, "Error in checkForFontAndColors()", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateBackgroundColor() {
|
||||||
|
if (!mActivity.isVisible()) return;
|
||||||
|
TerminalSession session = mActivity.getCurrentSession();
|
||||||
|
if (session != null && session.getEmulator() != null) {
|
||||||
|
mActivity.getWindow().getDecorView().setBackgroundColor(session.getEmulator().mColors.mCurrentColors[TextStyle.COLOR_INDEX_BACKGROUND]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,443 @@
|
|||||||
|
package com.termux.app.terminal;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.ActivityNotFoundException;
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.ClipboardManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.media.AudioManager;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.InputDevice;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
import android.widget.ListView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.app.TermuxActivity;
|
||||||
|
import com.termux.shared.shell.ShellUtils;
|
||||||
|
import com.termux.shared.terminal.TermuxTerminalViewClientBase;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
import com.termux.app.activities.ReportActivity;
|
||||||
|
import com.termux.app.models.ReportInfo;
|
||||||
|
import com.termux.app.models.UserAction;
|
||||||
|
import com.termux.app.terminal.io.KeyboardShortcut;
|
||||||
|
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
|
||||||
|
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||||
|
import com.termux.shared.data.DataUtils;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.markdown.MarkdownUtils;
|
||||||
|
import com.termux.shared.termux.TermuxUtils;
|
||||||
|
import com.termux.terminal.KeyHandler;
|
||||||
|
import com.termux.terminal.TerminalEmulator;
|
||||||
|
import com.termux.terminal.TerminalSession;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
|
|
||||||
|
public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||||
|
|
||||||
|
final TermuxActivity mActivity;
|
||||||
|
|
||||||
|
final TermuxTerminalSessionClient mTermuxTerminalSessionClient;
|
||||||
|
|
||||||
|
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
|
||||||
|
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
|
||||||
|
|
||||||
|
public TermuxTerminalViewClient(TermuxActivity activity, TermuxTerminalSessionClient termuxTerminalSessionClient) {
|
||||||
|
this.mActivity = activity;
|
||||||
|
this.mTermuxTerminalSessionClient = termuxTerminalSessionClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float onScale(float scale) {
|
||||||
|
if (scale < 0.9f || scale > 1.1f) {
|
||||||
|
boolean increase = scale > 1.f;
|
||||||
|
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.getTerminalView(), InputMethodManager.SHOW_IMPLICIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldBackButtonBeMappedToEscape() {
|
||||||
|
return mActivity.getProperties().isBackKeyTheEscapeKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldEnforceCharBasedInput() {
|
||||||
|
return mActivity.getProperties().isEnforcingCharBasedInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldUseCtrlSpaceWorkaround() {
|
||||||
|
return mActivity.getProperties().isUsingCtrlSpaceWorkaround();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void copyModeChanged(boolean copyMode) {
|
||||||
|
// Disable drawer while copying.
|
||||||
|
mActivity.getDrawer().setDrawerLockMode(copyMode ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED : DrawerLayout.LOCK_MODE_UNLOCKED);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressLint("RtlHardcoded")
|
||||||
|
@Override
|
||||||
|
public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession currentSession) {
|
||||||
|
if (handleVirtualKeys(keyCode, e, true)) return true;
|
||||||
|
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) {
|
||||||
|
mTermuxTerminalSessionClient.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 */) {
|
||||||
|
mTermuxTerminalSessionClient.switchToSession(true);
|
||||||
|
} else if (keyCode == KeyEvent.KEYCODE_DPAD_UP || unicodeChar == 'p' /* previous */) {
|
||||||
|
mTermuxTerminalSessionClient.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 == 'k'/* keyboard */) {
|
||||||
|
InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
|
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
|
||||||
|
} else if (unicodeChar == 'm'/* menu */) {
|
||||||
|
mActivity.getTerminalView().showContextMenu();
|
||||||
|
} else if (unicodeChar == 'r'/* rename */) {
|
||||||
|
mTermuxTerminalSessionClient.renameSession(currentSession);
|
||||||
|
} else if (unicodeChar == 'c'/* create */) {
|
||||||
|
mTermuxTerminalSessionClient.addNewSession(false, null);
|
||||||
|
} else if (unicodeChar == 'u' /* urls */) {
|
||||||
|
showUrlSelection();
|
||||||
|
} else if (unicodeChar == 'v') {
|
||||||
|
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
|
||||||
|
changeFontSize(true);
|
||||||
|
} else if (unicodeChar == '-') {
|
||||||
|
changeFontSize(false);
|
||||||
|
} else if (unicodeChar >= '1' && unicodeChar <= '9') {
|
||||||
|
int index = unicodeChar - '1';
|
||||||
|
mTermuxTerminalSessionClient.switchToSession(index);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onKeyUp(int keyCode, KeyEvent e) {
|
||||||
|
return handleVirtualKeys(keyCode, e, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Handle dedicated volume buttons as virtual keys if applicable. */
|
||||||
|
private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) {
|
||||||
|
InputDevice inputDevice = event.getDevice();
|
||||||
|
if (mActivity.getProperties().areVirtualVolumeKeysDisabled()) {
|
||||||
|
return false;
|
||||||
|
} else if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
|
||||||
|
// Do not steal dedicated buttons from a full external keyboard.
|
||||||
|
return false;
|
||||||
|
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
|
||||||
|
mVirtualControlKeyDown = down;
|
||||||
|
return true;
|
||||||
|
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
||||||
|
mVirtualFnKeyDown = down;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean readControlKey() {
|
||||||
|
return (mActivity.getExtraKeysView() != null && mActivity.getExtraKeysView().readSpecialButton(ExtraKeysView.SpecialButton.CTRL)) || mVirtualControlKeyDown;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean readAltKey() {
|
||||||
|
return (mActivity.getExtraKeysView() != null && mActivity.getExtraKeysView().readSpecialButton(ExtraKeysView.SpecialButton.ALT));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onLongPress(MotionEvent event) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@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':
|
||||||
|
case 'k':
|
||||||
|
mActivity.toggleTerminalToolbar();
|
||||||
|
mVirtualFnKeyDown=false; // force disable fn key down to restore keyboard input into terminal view, fixes termux/termux-app#1420
|
||||||
|
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()) {
|
||||||
|
mTermuxTerminalSessionClient.removeFinishedSession(session);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<KeyboardShortcut> shortcuts = mActivity.getProperties().getSessionShortcuts();
|
||||||
|
if (shortcuts != null && !shortcuts.isEmpty()) {
|
||||||
|
int codePointLowerCase = Character.toLowerCase(codePoint);
|
||||||
|
for (int i = shortcuts.size() - 1; i >= 0; i--) {
|
||||||
|
KeyboardShortcut shortcut = shortcuts.get(i);
|
||||||
|
if (codePointLowerCase == shortcut.codePoint) {
|
||||||
|
switch (shortcut.shortcutAction) {
|
||||||
|
case TermuxPropertyConstants.ACTION_SHORTCUT_CREATE_SESSION:
|
||||||
|
mTermuxTerminalSessionClient.addNewSession(false, null);
|
||||||
|
return true;
|
||||||
|
case TermuxPropertyConstants.ACTION_SHORTCUT_NEXT_SESSION:
|
||||||
|
mTermuxTerminalSessionClient.switchToSession(true);
|
||||||
|
return true;
|
||||||
|
case TermuxPropertyConstants.ACTION_SHORTCUT_PREVIOUS_SESSION:
|
||||||
|
mTermuxTerminalSessionClient.switchToSession(false);
|
||||||
|
return true;
|
||||||
|
case TermuxPropertyConstants.ACTION_SHORTCUT_RENAME_SESSION:
|
||||||
|
mTermuxTerminalSessionClient.renameSession(mActivity.getCurrentSession());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void changeFontSize(boolean increase) {
|
||||||
|
mActivity.getPreferences().changeFontSize(increase);
|
||||||
|
mActivity.getTerminalView().setTextSize(mActivity.getPreferences().getFontSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void shareSessionTranscript() {
|
||||||
|
TerminalSession session = mActivity.getCurrentSession();
|
||||||
|
if (session == null) return;
|
||||||
|
|
||||||
|
String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
|
||||||
|
if (transcriptText == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// See https://github.com/termux/termux-app/issues/1166.
|
||||||
|
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||||
|
intent.setType("text/plain");
|
||||||
|
transcriptText = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
|
||||||
|
intent.putExtra(Intent.EXTRA_TEXT, transcriptText);
|
||||||
|
intent.putExtra(Intent.EXTRA_SUBJECT, mActivity.getString(R.string.title_share_transcript));
|
||||||
|
mActivity.startActivity(Intent.createChooser(intent, mActivity.getString(R.string.title_share_transcript_with)));
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.logStackTraceWithMessage("Failed to get share session transcript of length " + transcriptText.length(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showUrlSelection() {
|
||||||
|
TerminalSession session = mActivity.getCurrentSession();
|
||||||
|
if (session == null) return;
|
||||||
|
|
||||||
|
String text = ShellUtils.getTerminalSessionTranscriptText(session, true, true);
|
||||||
|
|
||||||
|
LinkedHashSet<CharSequence> urlSet = DataUtils.extractUrls(text);
|
||||||
|
if (urlSet.isEmpty()) {
|
||||||
|
new AlertDialog.Builder(mActivity).setMessage(R.string.title_select_url_none_found).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final CharSequence[] urls = urlSet.toArray(new CharSequence[0]);
|
||||||
|
Collections.reverse(Arrays.asList(urls)); // Latest first.
|
||||||
|
|
||||||
|
// Click to copy url to clipboard:
|
||||||
|
final AlertDialog dialog = new AlertDialog.Builder(mActivity).setItems(urls, (di, which) -> {
|
||||||
|
String url = (String) urls[which];
|
||||||
|
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
|
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(url)));
|
||||||
|
Toast.makeText(mActivity, R.string.msg_select_url_copied_to_clipboard, Toast.LENGTH_LONG).show();
|
||||||
|
}).setTitle(R.string.title_select_url_dialog).create();
|
||||||
|
|
||||||
|
// Long press to open URL:
|
||||||
|
dialog.setOnShowListener(di -> {
|
||||||
|
ListView lv = dialog.getListView(); // this is a ListView with your "buds" in it
|
||||||
|
lv.setOnItemLongClickListener((parent, view, position, id) -> {
|
||||||
|
dialog.dismiss();
|
||||||
|
String url = (String) urls[position];
|
||||||
|
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||||
|
try {
|
||||||
|
mActivity.startActivity(i, null);
|
||||||
|
} catch (ActivityNotFoundException e) {
|
||||||
|
// If no applications match, Android displays a system message.
|
||||||
|
mActivity.startActivity(Intent.createChooser(i, null));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reportIssueFromTranscript() {
|
||||||
|
TerminalSession session = mActivity.getCurrentSession();
|
||||||
|
if (session == null) return;
|
||||||
|
|
||||||
|
String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
|
||||||
|
if (transcriptText == null) return;
|
||||||
|
|
||||||
|
transcriptText = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
|
||||||
|
|
||||||
|
StringBuilder reportString = new StringBuilder();
|
||||||
|
|
||||||
|
String title = TermuxConstants.TERMUX_APP_NAME + " Report Issue";
|
||||||
|
|
||||||
|
reportString.append("## Transcript\n");
|
||||||
|
reportString.append("\n").append(MarkdownUtils.getMarkdownCodeForString(transcriptText, true));
|
||||||
|
|
||||||
|
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, true));
|
||||||
|
reportString.append("\n\n").append(TermuxUtils.getDeviceInfoMarkdownString(mActivity));
|
||||||
|
|
||||||
|
String termuxAptInfo = TermuxUtils.geAPTInfoMarkdownString(mActivity);
|
||||||
|
if (termuxAptInfo != null)
|
||||||
|
reportString.append("\n\n").append(termuxAptInfo);
|
||||||
|
|
||||||
|
ReportActivity.startReportActivity(mActivity, new ReportInfo(UserAction.REPORT_ISSUE_FROM_TRANSCRIPT, TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY_NAME, title, null, reportString.toString(), "\n\n" + TermuxUtils.getReportIssueMarkdownString(mActivity), false));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void doPaste() {
|
||||||
|
TerminalSession session = mActivity.getCurrentSession();
|
||||||
|
if (session == null) return;
|
||||||
|
if (!session.isRunning()) return;
|
||||||
|
|
||||||
|
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
|
ClipData clipData = clipboard.getPrimaryClip();
|
||||||
|
if (clipData == null) return;
|
||||||
|
CharSequence paste = clipData.getItemAt(0).coerceToText(mActivity);
|
||||||
|
if (!TextUtils.isEmpty(paste))
|
||||||
|
session.getEmulator().paste(paste.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.termux.app;
|
package com.termux.app.terminal.io;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
@@ -6,15 +6,15 @@ import android.os.Looper;
|
|||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.os.Vibrator;
|
import android.os.Vibrator;
|
||||||
|
|
||||||
public class BellUtil {
|
public class BellHandler {
|
||||||
private static BellUtil instance = null;
|
private static BellHandler instance = null;
|
||||||
private static final Object lock = new Object();
|
private static final Object lock = new Object();
|
||||||
|
|
||||||
public static BellUtil getInstance(Context context) {
|
public static BellHandler getInstance(Context context) {
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
instance = new BellUtil((Vibrator) context.getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE));
|
instance = new BellHandler((Vibrator) context.getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ public class BellUtil {
|
|||||||
private long lastBell = 0;
|
private long lastBell = 0;
|
||||||
private final Runnable bellRunnable;
|
private final Runnable bellRunnable;
|
||||||
|
|
||||||
private BellUtil(final Vibrator vibrator) {
|
private BellHandler(final Vibrator vibrator) {
|
||||||
bellRunnable = new Runnable() {
|
bellRunnable = new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
@@ -47,7 +47,7 @@ public class BellUtil {
|
|||||||
if (timeSinceLastBell < 0) {
|
if (timeSinceLastBell < 0) {
|
||||||
// there is a next bell pending; don't schedule another one
|
// there is a next bell pending; don't schedule another one
|
||||||
} else if (timeSinceLastBell < MIN_PAUSE) {
|
} else if (timeSinceLastBell < MIN_PAUSE) {
|
||||||
// there was a bell recently, scheudle the next one
|
// there was a bell recently, schedule the next one
|
||||||
handler.postDelayed(bellRunnable, MIN_PAUSE - timeSinceLastBell);
|
handler.postDelayed(bellRunnable, MIN_PAUSE - timeSinceLastBell);
|
||||||
lastBell = lastBell + MIN_PAUSE;
|
lastBell = lastBell + MIN_PAUSE;
|
||||||
} else {
|
} else {
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package com.termux.app.terminal.io;
|
||||||
|
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import com.termux.app.TermuxActivity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Work around for fullscreen mode in Termux to fix ExtraKeysView not being visible.
|
||||||
|
* This class is derived from:
|
||||||
|
* https://stackoverflow.com/questions/7417123/android-how-to-adjust-layout-in-full-screen-mode-when-softkeyboard-is-visible
|
||||||
|
* and has some additional tweaks
|
||||||
|
* ---
|
||||||
|
* For more information, see https://issuetracker.google.com/issues/36911528
|
||||||
|
*/
|
||||||
|
public class FullScreenWorkAround {
|
||||||
|
private final View mChildOfContent;
|
||||||
|
private int mUsableHeightPrevious;
|
||||||
|
private final ViewGroup.LayoutParams mViewGroupLayoutParams;
|
||||||
|
|
||||||
|
private final int mNavBarHeight;
|
||||||
|
|
||||||
|
|
||||||
|
public static void apply(TermuxActivity activity) {
|
||||||
|
new FullScreenWorkAround(activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FullScreenWorkAround(TermuxActivity activity) {
|
||||||
|
ViewGroup content = activity.findViewById(android.R.id.content);
|
||||||
|
mChildOfContent = content.getChildAt(0);
|
||||||
|
mViewGroupLayoutParams = mChildOfContent.getLayoutParams();
|
||||||
|
mNavBarHeight = activity.getNavBarHeight();
|
||||||
|
mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(this::possiblyResizeChildOfContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void possiblyResizeChildOfContent() {
|
||||||
|
int usableHeightNow = computeUsableHeight();
|
||||||
|
if (usableHeightNow != mUsableHeightPrevious) {
|
||||||
|
int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
|
||||||
|
int heightDifference = usableHeightSansKeyboard - usableHeightNow;
|
||||||
|
if (heightDifference > (usableHeightSansKeyboard / 4)) {
|
||||||
|
// keyboard probably just became visible
|
||||||
|
|
||||||
|
// ensures that usable layout space does not extend behind the
|
||||||
|
// soft keyboard, causing the extra keys to not be visible
|
||||||
|
mViewGroupLayoutParams.height = (usableHeightSansKeyboard - heightDifference) + getNavBarHeight();
|
||||||
|
} else {
|
||||||
|
// keyboard probably just became hidden
|
||||||
|
mViewGroupLayoutParams.height = usableHeightSansKeyboard;
|
||||||
|
}
|
||||||
|
mChildOfContent.requestLayout();
|
||||||
|
mUsableHeightPrevious = usableHeightNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getNavBarHeight() {
|
||||||
|
return mNavBarHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int computeUsableHeight() {
|
||||||
|
Rect r = new Rect();
|
||||||
|
mChildOfContent.getWindowVisibleDisplayFrame(r);
|
||||||
|
return (r.bottom - r.top);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.termux.app.terminal.io;
|
||||||
|
|
||||||
|
public class KeyboardShortcut {
|
||||||
|
|
||||||
|
public final int codePoint;
|
||||||
|
public final int shortcutAction;
|
||||||
|
|
||||||
|
public KeyboardShortcut(int codePoint, int shortcutAction) {
|
||||||
|
this.codePoint = codePoint;
|
||||||
|
this.shortcutAction = shortcutAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package com.termux.app.terminal.io;
|
||||||
|
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.EditText;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.viewpager.widget.PagerAdapter;
|
||||||
|
import androidx.viewpager.widget.ViewPager;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.app.TermuxActivity;
|
||||||
|
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
|
||||||
|
import com.termux.terminal.TerminalSession;
|
||||||
|
|
||||||
|
public class TerminalToolbarViewPager {
|
||||||
|
|
||||||
|
public static class PageAdapter extends PagerAdapter {
|
||||||
|
|
||||||
|
final TermuxActivity mActivity;
|
||||||
|
String mSavedTextInput;
|
||||||
|
|
||||||
|
public PageAdapter(TermuxActivity activity, String savedTextInput) {
|
||||||
|
this.mActivity = activity;
|
||||||
|
this.mSavedTextInput = savedTextInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCount() {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
|
||||||
|
return view == object;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Object instantiateItem(@NonNull ViewGroup collection, int position) {
|
||||||
|
LayoutInflater inflater = LayoutInflater.from(mActivity);
|
||||||
|
View layout;
|
||||||
|
if (position == 0) {
|
||||||
|
layout = inflater.inflate(R.layout.view_terminal_toolbar_extra_keys, collection, false);
|
||||||
|
ExtraKeysView extraKeysView = (ExtraKeysView) layout;
|
||||||
|
mActivity.setExtraKeysView(extraKeysView);
|
||||||
|
extraKeysView.reload(mActivity.getProperties().getExtraKeysInfo());
|
||||||
|
|
||||||
|
// apply extra keys fix if enabled in prefs
|
||||||
|
if (mActivity.getProperties().isUsingFullScreen() && mActivity.getProperties().isUsingFullScreenWorkAround()) {
|
||||||
|
FullScreenWorkAround.apply(mActivity);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
layout = inflater.inflate(R.layout.view_terminal_toolbar_text_input, collection, false);
|
||||||
|
final EditText editText = layout.findViewById(R.id.terminal_toolbar_text_input);
|
||||||
|
|
||||||
|
if (mSavedTextInput != null) {
|
||||||
|
editText.setText(mSavedTextInput);
|
||||||
|
mSavedTextInput = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
editText.setOnEditorActionListener((v, actionId, event) -> {
|
||||||
|
TerminalSession session = mActivity.getCurrentSession();
|
||||||
|
if (session != null) {
|
||||||
|
if (session.isRunning()) {
|
||||||
|
String textToSend = editText.getText().toString();
|
||||||
|
if (textToSend.length() == 0) textToSend = "\r";
|
||||||
|
session.write(textToSend);
|
||||||
|
} else {
|
||||||
|
mActivity.getTermuxTerminalSessionClient().removeFinishedSession(session);
|
||||||
|
}
|
||||||
|
editText.setText("");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
collection.addView(layout);
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroyItem(@NonNull ViewGroup collection, int position, @NonNull Object view) {
|
||||||
|
collection.removeView((View) view);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static class OnPageChangeListener extends ViewPager.SimpleOnPageChangeListener {
|
||||||
|
|
||||||
|
final TermuxActivity mActivity;
|
||||||
|
final ViewPager mTerminalToolbarViewPager;
|
||||||
|
|
||||||
|
public OnPageChangeListener(TermuxActivity activity, ViewPager viewPager) {
|
||||||
|
this.mActivity = activity;
|
||||||
|
this.mTerminalToolbarViewPager = viewPager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPageSelected(int position) {
|
||||||
|
if (position == 0) {
|
||||||
|
mActivity.getTerminalView().requestFocus();
|
||||||
|
} else {
|
||||||
|
final EditText editText = mTerminalToolbarViewPager.findViewById(R.id.terminal_toolbar_text_input);
|
||||||
|
if (editText != null) editText.requestFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package com.termux.app.terminal.io.extrakeys;
|
||||||
|
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class ExtraKeyButton {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The key that will be sent to the terminal, either a control character
|
||||||
|
* defined in ExtraKeysView.keyCodesForString (LEFT, RIGHT, PGUP...) or
|
||||||
|
* some text.
|
||||||
|
*/
|
||||||
|
private final String key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the key is a macro, i.e. a sequence of keys separated by space.
|
||||||
|
*/
|
||||||
|
private final boolean macro;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The text that will be shown on the button.
|
||||||
|
*/
|
||||||
|
private final String display;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The information of the popup (triggered by swipe up).
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private ExtraKeyButton popup;
|
||||||
|
|
||||||
|
public ExtraKeyButton(ExtraKeysInfo.CharDisplayMap charDisplayMap, JSONObject config) throws JSONException {
|
||||||
|
this(charDisplayMap, config, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtraKeyButton(ExtraKeysInfo.CharDisplayMap charDisplayMap, JSONObject config, @Nullable ExtraKeyButton popup) throws JSONException {
|
||||||
|
String keyFromConfig = config.optString("key", null);
|
||||||
|
String macroFromConfig = config.optString("macro", null);
|
||||||
|
String[] keys;
|
||||||
|
if (keyFromConfig != null && macroFromConfig != null) {
|
||||||
|
throw new JSONException("Both key and macro can't be set for the same key");
|
||||||
|
} else if (keyFromConfig != null) {
|
||||||
|
keys = new String[]{keyFromConfig};
|
||||||
|
this.macro = false;
|
||||||
|
} else if (macroFromConfig != null) {
|
||||||
|
keys = macroFromConfig.split(" ");
|
||||||
|
this.macro = true;
|
||||||
|
} else {
|
||||||
|
throw new JSONException("All keys have to specify either key or macro");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < keys.length; i++) {
|
||||||
|
keys[i] = ExtraKeysInfo.replaceAlias(keys[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.key = TextUtils.join(" ", keys);
|
||||||
|
|
||||||
|
String displayFromConfig = config.optString("display", null);
|
||||||
|
if (displayFromConfig != null) {
|
||||||
|
this.display = displayFromConfig;
|
||||||
|
} else {
|
||||||
|
this.display = Arrays.stream(keys)
|
||||||
|
.map(key -> charDisplayMap.get(key, key))
|
||||||
|
.collect(Collectors.joining(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.popup = popup;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMacro() {
|
||||||
|
return macro;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplay() {
|
||||||
|
return display;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public ExtraKeyButton getPopup() {
|
||||||
|
return popup;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,31 +1,24 @@
|
|||||||
package com.termux.app;
|
package com.termux.app.terminal.io.extrakeys;
|
||||||
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public class ExtraKeysInfos {
|
public class ExtraKeysInfo {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Matrix of buttons displayed
|
* Matrix of buttons displayed
|
||||||
*/
|
*/
|
||||||
private ExtraKeyButton[][] buttons;
|
private final ExtraKeyButton[][] buttons;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This corresponds to one of the CharMapDisplay below
|
* This corresponds to one of the CharMapDisplay below
|
||||||
*/
|
*/
|
||||||
private String style = "default";
|
private String style;
|
||||||
|
|
||||||
public ExtraKeysInfos(String propertiesInfo, String style) throws JSONException {
|
public ExtraKeysInfo(String propertiesInfo, String style) throws JSONException {
|
||||||
this.style = style;
|
this.style = style;
|
||||||
|
|
||||||
// Convert String propertiesInfo to Array of Arrays
|
// Convert String propertiesInfo to Array of Arrays
|
||||||
@@ -50,7 +43,7 @@ public class ExtraKeysInfos {
|
|||||||
|
|
||||||
ExtraKeyButton button;
|
ExtraKeyButton button;
|
||||||
|
|
||||||
if(! jobject.has("popup")) {
|
if (! jobject.has("popup")) {
|
||||||
// no popup
|
// no popup
|
||||||
button = new ExtraKeyButton(getSelectedCharMap(), jobject);
|
button = new ExtraKeyButton(getSelectedCharMap(), jobject);
|
||||||
} else {
|
} else {
|
||||||
@@ -70,10 +63,10 @@ public class ExtraKeysInfos {
|
|||||||
*/
|
*/
|
||||||
private static JSONObject normalizeKeyConfig(Object key) throws JSONException {
|
private static JSONObject normalizeKeyConfig(Object key) throws JSONException {
|
||||||
JSONObject jobject;
|
JSONObject jobject;
|
||||||
if(key instanceof String) {
|
if (key instanceof String) {
|
||||||
jobject = new JSONObject();
|
jobject = new JSONObject();
|
||||||
jobject.put("key", key);
|
jobject.put("key", key);
|
||||||
} else if(key instanceof JSONObject) {
|
} else if (key instanceof JSONObject) {
|
||||||
jobject = (JSONObject) key;
|
jobject = (JSONObject) key;
|
||||||
} else {
|
} else {
|
||||||
throw new JSONException("An key in the extra-key matrix must be a string or an object");
|
throw new JSONException("An key in the extra-key matrix must be a string or an object");
|
||||||
@@ -91,7 +84,7 @@ public class ExtraKeysInfos {
|
|||||||
*/
|
*/
|
||||||
static class CleverMap<K,V> extends HashMap<K,V> {
|
static class CleverMap<K,V> extends HashMap<K,V> {
|
||||||
V get(K key, V defaultValue) {
|
V get(K key, V defaultValue) {
|
||||||
if(containsKey(key))
|
if (containsKey(key))
|
||||||
return get(key);
|
return get(key);
|
||||||
else
|
else
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
@@ -151,7 +144,7 @@ public class ExtraKeysInfos {
|
|||||||
put("-", "―"); // U+2015 ― HORIZONTAL BAR
|
put("-", "―"); // U+2015 ― HORIZONTAL BAR
|
||||||
}};
|
}};
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Multiple maps are available to quickly change
|
* Multiple maps are available to quickly change
|
||||||
* the style of the keys.
|
* the style of the keys.
|
||||||
*/
|
*/
|
||||||
@@ -258,83 +251,3 @@ public class ExtraKeysInfos {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExtraKeyButton {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The key that will be sent to the terminal, either a control character
|
|
||||||
* defined in ExtraKeysView.keyCodesForString (LEFT, RIGHT, PGUP...) or
|
|
||||||
* some text.
|
|
||||||
*/
|
|
||||||
private String key;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If the key is a macro, i.e. a sequence of keys separated by space.
|
|
||||||
*/
|
|
||||||
private boolean macro;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The text that will be shown on the button.
|
|
||||||
*/
|
|
||||||
private String display;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The information of the popup (triggered by swipe up).
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
private ExtraKeyButton popup = null;
|
|
||||||
|
|
||||||
public ExtraKeyButton(ExtraKeysInfos.CharDisplayMap charDisplayMap, JSONObject config) throws JSONException {
|
|
||||||
this(charDisplayMap, config, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ExtraKeyButton(ExtraKeysInfos.CharDisplayMap charDisplayMap, JSONObject config, ExtraKeyButton popup) throws JSONException {
|
|
||||||
String keyFromConfig = config.optString("key", null);
|
|
||||||
String macroFromConfig = config.optString("macro", null);
|
|
||||||
String[] keys;
|
|
||||||
if (keyFromConfig != null && macroFromConfig != null) {
|
|
||||||
throw new JSONException("Both key and macro can't be set for the same key");
|
|
||||||
} else if (keyFromConfig != null) {
|
|
||||||
keys = new String[]{keyFromConfig};
|
|
||||||
this.macro = false;
|
|
||||||
} else if (macroFromConfig != null) {
|
|
||||||
keys = macroFromConfig.split(" ");
|
|
||||||
this.macro = true;
|
|
||||||
} else {
|
|
||||||
throw new JSONException("All keys have to specify either key or macro");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < keys.length; i++) {
|
|
||||||
keys[i] = ExtraKeysInfos.replaceAlias(keys[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.key = TextUtils.join(" ", keys);
|
|
||||||
|
|
||||||
String displayFromConfig = config.optString("display", null);
|
|
||||||
if (displayFromConfig != null) {
|
|
||||||
this.display = displayFromConfig;
|
|
||||||
} else {
|
|
||||||
this.display = Arrays.stream(keys)
|
|
||||||
.map(key -> charDisplayMap.get(key, key))
|
|
||||||
.collect(Collectors.joining(" "));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.popup = popup;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getKey() {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isMacro() {
|
|
||||||
return macro;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDisplay() {
|
|
||||||
return display;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public ExtraKeyButton getPopup() {
|
|
||||||
return popup;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.termux.app;
|
package com.termux.app.terminal.io.extrakeys;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@@ -6,6 +6,9 @@ import android.os.Build;
|
|||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
@@ -13,6 +16,7 @@ import java.util.concurrent.ScheduledExecutorService;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
import android.view.HapticFeedbackConstants;
|
import android.view.HapticFeedbackConstants;
|
||||||
@@ -23,7 +27,6 @@ import android.view.inputmethod.InputMethodManager;
|
|||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.GridLayout;
|
import android.widget.GridLayout;
|
||||||
import android.widget.PopupWindow;
|
import android.widget.PopupWindow;
|
||||||
import android.widget.ToggleButton;
|
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.view.TerminalView;
|
import com.termux.view.TerminalView;
|
||||||
@@ -75,6 +78,7 @@ public final class ExtraKeysView extends GridLayout {
|
|||||||
put("F12", KeyEvent.KEYCODE_F12);
|
put("F12", KeyEvent.KEYCODE_F12);
|
||||||
}};
|
}};
|
||||||
|
|
||||||
|
@SuppressLint("RtlHardcoded")
|
||||||
private void sendKey(View view, String keyName, boolean forceCtrlDown, boolean forceLeftAltDown) {
|
private void sendKey(View view, String keyName, boolean forceCtrlDown, boolean forceLeftAltDown) {
|
||||||
TerminalView terminalView = view.findViewById(R.id.terminal_view);
|
TerminalView terminalView = view.findViewById(R.id.terminal_view);
|
||||||
if ("KEYBOARD".equals(keyName)) {
|
if ("KEYBOARD".equals(keyName)) {
|
||||||
@@ -84,7 +88,8 @@ public final class ExtraKeysView extends GridLayout {
|
|||||||
DrawerLayout drawer = view.findViewById(R.id.drawer_layout);
|
DrawerLayout drawer = view.findViewById(R.id.drawer_layout);
|
||||||
drawer.openDrawer(Gravity.LEFT);
|
drawer.openDrawer(Gravity.LEFT);
|
||||||
} else if (keyCodesForString.containsKey(keyName)) {
|
} else if (keyCodesForString.containsKey(keyName)) {
|
||||||
int keyCode = keyCodesForString.get(keyName);
|
Integer keyCode = keyCodesForString.get(keyName);
|
||||||
|
if (keyCode == null) return;
|
||||||
int metaState = 0;
|
int metaState = 0;
|
||||||
if (forceCtrlDown) {
|
if (forceCtrlDown) {
|
||||||
metaState |= KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON;
|
metaState |= KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON;
|
||||||
@@ -129,15 +134,27 @@ public final class ExtraKeysView extends GridLayout {
|
|||||||
|
|
||||||
private static class SpecialButtonState {
|
private static class SpecialButtonState {
|
||||||
boolean isOn = false;
|
boolean isOn = false;
|
||||||
ToggleButton button = null;
|
boolean isActive = false;
|
||||||
|
List<Button> buttons = new ArrayList<>();
|
||||||
|
|
||||||
|
void setIsActive(boolean value) {
|
||||||
|
isActive = value;
|
||||||
|
buttons.forEach(button -> button.setTextColor(value ? INTERESTING_COLOR : TEXT_COLOR));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<SpecialButton, SpecialButtonState> specialButtons = new HashMap<SpecialButton, SpecialButtonState>() {{
|
private final Map<SpecialButton, SpecialButtonState> specialButtons = new HashMap<SpecialButton, SpecialButtonState>() {{
|
||||||
put(SpecialButton.CTRL, new SpecialButtonState());
|
put(SpecialButton.CTRL, new SpecialButtonState());
|
||||||
put(SpecialButton.ALT, new SpecialButtonState());
|
put(SpecialButton.ALT, new SpecialButtonState());
|
||||||
put(SpecialButton.FN, new SpecialButtonState());
|
put(SpecialButton.FN, new SpecialButtonState());
|
||||||
}};
|
}};
|
||||||
|
|
||||||
|
private final Set<String> specialButtonsKeys = specialButtons.keySet().stream().map(Enum::name).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
private boolean isSpecialButton(ExtraKeyButton button) {
|
||||||
|
return specialButtonsKeys.contains(button.getKey());
|
||||||
|
}
|
||||||
|
|
||||||
private ScheduledExecutorService scheduledExecutor;
|
private ScheduledExecutorService scheduledExecutor;
|
||||||
private PopupWindow popupWindow;
|
private PopupWindow popupWindow;
|
||||||
private int longPressCount;
|
private int longPressCount;
|
||||||
@@ -147,30 +164,38 @@ public final class ExtraKeysView extends GridLayout {
|
|||||||
if (state == null)
|
if (state == null)
|
||||||
throw new RuntimeException("Must be a valid special button (see source)");
|
throw new RuntimeException("Must be a valid special button (see source)");
|
||||||
|
|
||||||
if (! state.isOn)
|
if (!state.isOn || !state.isActive)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (state.button == null) {
|
state.setIsActive(false);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.button.isPressed())
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (! state.button.isChecked())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
state.button.setChecked(false);
|
|
||||||
state.button.setTextColor(TEXT_COLOR);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void popup(View view, String text) {
|
private Button createSpecialButton(String buttonKey, boolean needUpdate) {
|
||||||
|
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonKey));
|
||||||
|
if (state == null) return null;
|
||||||
|
state.isOn = true;
|
||||||
|
Button button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||||
|
button.setTextColor(state.isActive ? INTERESTING_COLOR : TEXT_COLOR);
|
||||||
|
if (needUpdate) {
|
||||||
|
state.buttons.add(button);
|
||||||
|
}
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
void popup(View view, ExtraKeyButton extraButton) {
|
||||||
int width = view.getMeasuredWidth();
|
int width = view.getMeasuredWidth();
|
||||||
int height = view.getMeasuredHeight();
|
int height = view.getMeasuredHeight();
|
||||||
Button button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
Button button;
|
||||||
button.setText(text);
|
if (isSpecialButton(extraButton)) {
|
||||||
button.setTextColor(TEXT_COLOR);
|
button = createSpecialButton(extraButton.getKey(), false);
|
||||||
|
if (button == null) return;
|
||||||
|
} else {
|
||||||
|
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||||
|
button.setTextColor(TEXT_COLOR);
|
||||||
|
}
|
||||||
|
button.setText(extraButton.getDisplay());
|
||||||
button.setPadding(0, 0, 0, 0);
|
button.setPadding(0, 0, 0, 0);
|
||||||
button.setMinHeight(0);
|
button.setMinHeight(0);
|
||||||
button.setMinWidth(0);
|
button.setMinWidth(0);
|
||||||
@@ -214,12 +239,12 @@ public final class ExtraKeysView extends GridLayout {
|
|||||||
* "-_-" will input the string "-_-"
|
* "-_-" will input the string "-_-"
|
||||||
*/
|
*/
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
void reload(ExtraKeysInfos infos) {
|
public void reload(ExtraKeysInfo infos) {
|
||||||
if(infos == null)
|
if (infos == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
for(SpecialButtonState state : specialButtons.values())
|
for(SpecialButtonState state : specialButtons.values())
|
||||||
state.button = null;
|
state.buttons = new ArrayList<>();
|
||||||
|
|
||||||
removeAllViews();
|
removeAllViews();
|
||||||
|
|
||||||
@@ -233,11 +258,9 @@ public final class ExtraKeysView extends GridLayout {
|
|||||||
final ExtraKeyButton buttonInfo = buttons[row][col];
|
final ExtraKeyButton buttonInfo = buttons[row][col];
|
||||||
|
|
||||||
Button button;
|
Button button;
|
||||||
if(Arrays.asList("CTRL", "ALT", "FN").contains(buttonInfo.getKey())) {
|
if (isSpecialButton(buttonInfo)) {
|
||||||
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonInfo.getKey())); // for valueOf: https://stackoverflow.com/a/604426/1980630
|
button = createSpecialButton(buttonInfo.getKey(), true);
|
||||||
state.isOn = true;
|
if (button == null) return;
|
||||||
button = state.button = new ToggleButton(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
|
||||||
button.setClickable(true);
|
|
||||||
} else {
|
} else {
|
||||||
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||||
}
|
}
|
||||||
@@ -262,10 +285,10 @@ public final class ExtraKeysView extends GridLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
View root = getRootView();
|
View root = getRootView();
|
||||||
if (Arrays.asList("CTRL", "ALT", "FN").contains(buttonInfo.getKey())) {
|
if (isSpecialButton(buttonInfo)) {
|
||||||
ToggleButton self = (ToggleButton) finalButton;
|
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonInfo.getKey()));
|
||||||
self.setChecked(self.isChecked());
|
if (state == null) return;
|
||||||
self.setTextColor(self.isChecked() ? INTERESTING_COLOR : TEXT_COLOR);
|
state.setIsActive(!state.isActive);
|
||||||
} else {
|
} else {
|
||||||
sendKey(root, buttonInfo);
|
sendKey(root, buttonInfo);
|
||||||
}
|
}
|
||||||
@@ -295,8 +318,7 @@ public final class ExtraKeysView extends GridLayout {
|
|||||||
scheduledExecutor = null;
|
scheduledExecutor = null;
|
||||||
}
|
}
|
||||||
v.setBackgroundColor(BUTTON_COLOR);
|
v.setBackgroundColor(BUTTON_COLOR);
|
||||||
String extraButtonDisplayedText = buttonInfo.getPopup().getDisplay();
|
popup(v, buttonInfo.getPopup());
|
||||||
popup(v, extraButtonDisplayedText);
|
|
||||||
}
|
}
|
||||||
if (popupWindow != null && event.getY() > 0) {
|
if (popupWindow != null && event.getY() > 0) {
|
||||||
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
|
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
|
||||||
@@ -325,7 +347,13 @@ public final class ExtraKeysView extends GridLayout {
|
|||||||
popupWindow.dismiss();
|
popupWindow.dismiss();
|
||||||
popupWindow = null;
|
popupWindow = null;
|
||||||
if (buttonInfo.getPopup() != null) {
|
if (buttonInfo.getPopup() != null) {
|
||||||
sendKey(root, buttonInfo.getPopup());
|
if (isSpecialButton(buttonInfo.getPopup())) {
|
||||||
|
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonInfo.getPopup().getKey()));
|
||||||
|
if (state == null) return true;
|
||||||
|
state.setIsActive(!state.isActive);
|
||||||
|
} else {
|
||||||
|
sendKey(root, buttonInfo.getPopup());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
v.performClick();
|
v.performClick();
|
||||||
156
app/src/main/java/com/termux/app/utils/CrashUtils.java
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
package com.termux.app.utils;
|
||||||
|
|
||||||
|
import android.app.Notification;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.app.activities.ReportActivity;
|
||||||
|
import com.termux.shared.notification.NotificationUtils;
|
||||||
|
import com.termux.shared.file.FileUtils;
|
||||||
|
import com.termux.app.models.ReportInfo;
|
||||||
|
import com.termux.app.models.UserAction;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxPreferenceConstants;
|
||||||
|
import com.termux.shared.data.DataUtils;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.termux.TermuxUtils;
|
||||||
|
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
|
||||||
|
public class CrashUtils {
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "CrashUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify the user of a previous app crash by reading the crash info from the crash log file at
|
||||||
|
* {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}. The crash log file would have been
|
||||||
|
* created by {@link com.termux.shared.crash.CrashHandler}.
|
||||||
|
*
|
||||||
|
* If the crash log file exists and is not empty and
|
||||||
|
* {@link TermuxPreferenceConstants.TERMUX_APP#KEY_CRASH_REPORT_NOTIFICATIONS_ENABLED} is
|
||||||
|
* enabled, then a notification will be shown for the crash on the
|
||||||
|
* {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME} channel, otherwise nothing will be done.
|
||||||
|
*
|
||||||
|
* After reading from the crash log file, it will be moved to {@link TermuxConstants#TERMUX_CRASH_LOG_BACKUP_FILE_PATH}.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
* @param logTagParam The log tag to use for logging.
|
||||||
|
*/
|
||||||
|
public static void notifyCrash(final Context context, final String logTagParam) {
|
||||||
|
if (context == null) return;
|
||||||
|
|
||||||
|
|
||||||
|
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(context);
|
||||||
|
// If user has disabled notifications for crashes
|
||||||
|
if (!preferences.getCrashReportNotificationsEnabled())
|
||||||
|
return;
|
||||||
|
|
||||||
|
new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
String logTag = DataUtils.getDefaultIfNull(logTagParam, LOG_TAG);
|
||||||
|
|
||||||
|
if (!FileUtils.regularFileExists(TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, false))
|
||||||
|
return;
|
||||||
|
|
||||||
|
String errmsg;
|
||||||
|
StringBuilder reportStringBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
// Read report string from crash log file
|
||||||
|
errmsg = FileUtils.readStringFromFile(context, "crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, Charset.defaultCharset(), reportStringBuilder, false);
|
||||||
|
if (errmsg != null) {
|
||||||
|
Logger.logError(logTag, errmsg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move crash log file to backup location if it exists
|
||||||
|
FileUtils.moveRegularFile(context, "crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, TermuxConstants.TERMUX_CRASH_LOG_BACKUP_FILE_PATH, true);
|
||||||
|
if (errmsg != null) {
|
||||||
|
Logger.logError(logTag, errmsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
String reportString = reportStringBuilder.toString();
|
||||||
|
|
||||||
|
if (reportString == null || reportString.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Send a notification to show the crash log which when clicked will open the {@link ReportActivity}
|
||||||
|
// to show the details of the crash
|
||||||
|
String title = TermuxConstants.TERMUX_APP_NAME + " Crash Report";
|
||||||
|
|
||||||
|
Logger.logDebug(logTag, "The crash log file at \"" + TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH + "\" found. Sending \"" + title + "\" notification.");
|
||||||
|
|
||||||
|
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.CRASH_REPORT, logTag, title, null, reportString, "\n\n" + TermuxUtils.getReportIssueMarkdownString(context), true));
|
||||||
|
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
|
||||||
|
// Setup the notification channel if not already set up
|
||||||
|
setupCrashReportsNotificationChannel(context);
|
||||||
|
|
||||||
|
// Build the notification
|
||||||
|
Notification.Builder builder = getCrashReportsNotificationBuilder(context, title, null, null, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
|
||||||
|
if (builder == null) return;
|
||||||
|
|
||||||
|
// Send the notification
|
||||||
|
int nextNotificationId = NotificationUtils.getNextNotificationId(context);
|
||||||
|
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
|
||||||
|
if (notificationManager != null)
|
||||||
|
notificationManager.notify(nextNotificationId, builder.build());
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID}
|
||||||
|
* and {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
* @param title The title for the notification.
|
||||||
|
* @param notificationText The second line text of the notification.
|
||||||
|
* @param notificationBigText The full text of the notification that may optionally be styled.
|
||||||
|
* @param pendingIntent The {@link PendingIntent} which should be sent when notification is clicked.
|
||||||
|
* @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}.
|
||||||
|
* @return Returns the {@link Notification.Builder}.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static Notification.Builder getCrashReportsNotificationBuilder(final Context context, final CharSequence title, final CharSequence notificationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) {
|
||||||
|
|
||||||
|
Notification.Builder builder = NotificationUtils.geNotificationBuilder(context,
|
||||||
|
TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_HIGH,
|
||||||
|
title, notificationText, notificationBigText, pendingIntent, notificationMode);
|
||||||
|
|
||||||
|
if (builder == null) return null;
|
||||||
|
|
||||||
|
// Enable timestamp
|
||||||
|
builder.setShowWhen(true);
|
||||||
|
|
||||||
|
// Set notification icon
|
||||||
|
builder.setSmallIcon(R.drawable.ic_error_notification);
|
||||||
|
|
||||||
|
// Set background color for small notification icon
|
||||||
|
builder.setColor(0xFF607D8B);
|
||||||
|
|
||||||
|
// Dismiss on click
|
||||||
|
builder.setAutoCancel(true);
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup the notification channel for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID} and
|
||||||
|
* {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
*/
|
||||||
|
public static void setupCrashReportsNotificationChannel(final Context context) {
|
||||||
|
NotificationUtils.setupNotificationChannel(context, TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID,
|
||||||
|
TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
330
app/src/main/java/com/termux/app/utils/PluginUtils.java
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
package com.termux.app.utils;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.Notification;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.shared.notification.NotificationUtils;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||||
|
import com.termux.app.activities.ReportActivity;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_APP;
|
||||||
|
import com.termux.shared.settings.properties.SharedProperties;
|
||||||
|
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||||
|
import com.termux.app.models.ReportInfo;
|
||||||
|
import com.termux.shared.models.ExecutionCommand;
|
||||||
|
import com.termux.app.models.UserAction;
|
||||||
|
import com.termux.shared.data.DataUtils;
|
||||||
|
import com.termux.shared.markdown.MarkdownUtils;
|
||||||
|
import com.termux.shared.termux.TermuxUtils;
|
||||||
|
|
||||||
|
public class PluginUtils {
|
||||||
|
|
||||||
|
/** Required file permissions for the executable file of execute intent. Executable file must have read and execute permissions */
|
||||||
|
public static final String PLUGIN_EXECUTABLE_FILE_PERMISSIONS = "r-x"; // Default: "r-x"
|
||||||
|
/** Required file permissions for the working directory of execute intent. Working directory must have read and write permissions.
|
||||||
|
* Execute permissions should be attempted to be set, but ignored if they are missing */
|
||||||
|
public static final String PLUGIN_WORKING_DIRECTORY_PERMISSIONS = "rwx"; // Default: "rwx"
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "PluginUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process {@link ExecutionCommand} result.
|
||||||
|
*
|
||||||
|
* The ExecutionCommand currentState must be greater or equal to
|
||||||
|
* {@link ExecutionCommand.ExecutionState#EXECUTED}.
|
||||||
|
* If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and
|
||||||
|
* {@link ExecutionCommand#pluginPendingIntent} is not {@code null}, then the result of commands
|
||||||
|
* are sent back to the {@link PendingIntent} creator.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} that will be used to send result intent to the {@link PendingIntent} creator.
|
||||||
|
* @param logTag The log tag to use for logging.
|
||||||
|
* @param executionCommand The {@link ExecutionCommand} to process.
|
||||||
|
*/
|
||||||
|
public static void processPluginExecutionCommandResult(final Context context, String logTag, final ExecutionCommand executionCommand) {
|
||||||
|
if (executionCommand == null) return;
|
||||||
|
|
||||||
|
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||||
|
|
||||||
|
if (!executionCommand.hasExecuted()) {
|
||||||
|
Logger.logWarn(logTag, "Ignoring call to processPluginExecutionCommandResult() since the execution command state is not higher than the ExecutionState.EXECUTED");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.logDebug(LOG_TAG, executionCommand.toString());
|
||||||
|
|
||||||
|
boolean result = true;
|
||||||
|
|
||||||
|
// If isPluginExecutionCommand is true and pluginPendingIntent is not null, then
|
||||||
|
// send pluginPendingIntent to its creator with the result
|
||||||
|
if (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null) {
|
||||||
|
String errmsg = executionCommand.errmsg;
|
||||||
|
|
||||||
|
//Combine errmsg and stacktraces
|
||||||
|
if (executionCommand.isStateFailed()) {
|
||||||
|
errmsg = Logger.getMessageAndStackTracesString(executionCommand.errmsg, executionCommand.throwableList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send pluginPendingIntent to its creator
|
||||||
|
result = sendPluginExecutionCommandResultPendingIntent(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.stdout, executionCommand.stderr, executionCommand.exitCode, executionCommand.errCode, errmsg, executionCommand.pluginPendingIntent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!executionCommand.isStateFailed() && result)
|
||||||
|
executionCommand.setState(ExecutionCommand.ExecutionState.SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process {@link ExecutionCommand} error.
|
||||||
|
*
|
||||||
|
* The ExecutionCommand currentState must be equal to {@link ExecutionCommand.ExecutionState#FAILED}.
|
||||||
|
* The {@link ExecutionCommand#errCode} must have been set to a value greater than
|
||||||
|
* {@link ExecutionCommand#RESULT_CODE_OK}.
|
||||||
|
* The {@link ExecutionCommand#errmsg} and any {@link ExecutionCommand#throwableList} must also
|
||||||
|
* be set with appropriate error info.
|
||||||
|
*
|
||||||
|
* If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and
|
||||||
|
* {@link ExecutionCommand#pluginPendingIntent} is not {@code null}, then the errors of commands
|
||||||
|
* are sent back to the {@link PendingIntent} creator.
|
||||||
|
*
|
||||||
|
* Otherwise if the {@link TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED} is
|
||||||
|
* enabled, then a flash and a notification will be shown for the error as well
|
||||||
|
* on the {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME} channel instead of just logging
|
||||||
|
* the error.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
* @param logTag The log tag to use for logging.
|
||||||
|
* @param executionCommand The {@link ExecutionCommand} that failed.
|
||||||
|
* @param forceNotification If set to {@code true}, then a flash and notification will be shown
|
||||||
|
* regardless of if pending intent is {@code null} or
|
||||||
|
* {@link TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED}
|
||||||
|
* is {@code false}.
|
||||||
|
*/
|
||||||
|
public static void processPluginExecutionCommandError(final Context context, String logTag, final ExecutionCommand executionCommand, boolean forceNotification) {
|
||||||
|
if (context == null || executionCommand == null) return;
|
||||||
|
|
||||||
|
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||||
|
|
||||||
|
if (!executionCommand.isStateFailed()) {
|
||||||
|
Logger.logWarn(logTag, "Ignoring call to processPluginExecutionCommandError() since the execution command is not in ExecutionState.FAILED");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the error and any exception
|
||||||
|
Logger.logStackTracesWithMessage(logTag, "(" + executionCommand.errCode + ") " + executionCommand.errmsg, executionCommand.throwableList);
|
||||||
|
|
||||||
|
|
||||||
|
// If isPluginExecutionCommand is true and pluginPendingIntent is not null, then
|
||||||
|
// send pluginPendingIntent to its creator with the errors
|
||||||
|
if (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null) {
|
||||||
|
String errmsg = executionCommand.errmsg;
|
||||||
|
|
||||||
|
//Combine errmsg and stacktraces
|
||||||
|
if (executionCommand.isStateFailed()) {
|
||||||
|
errmsg = Logger.getMessageAndStackTracesString(executionCommand.errmsg, executionCommand.throwableList);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendPluginExecutionCommandResultPendingIntent(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.stdout, executionCommand.stderr, executionCommand.exitCode, executionCommand.errCode, errmsg, executionCommand.pluginPendingIntent);
|
||||||
|
|
||||||
|
// No need to show notifications if a pending intent was sent, let the caller handle the result himself
|
||||||
|
if (!forceNotification) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(context);
|
||||||
|
// If user has disabled notifications for plugin, then just return
|
||||||
|
if (!preferences.getPluginErrorNotificationsEnabled() && !forceNotification)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Flash the errmsg
|
||||||
|
Logger.showToast(context, executionCommand.errmsg, true);
|
||||||
|
|
||||||
|
// Send a notification to show the errmsg which when clicked will open the {@link ReportActivity}
|
||||||
|
// to show the details of the error
|
||||||
|
String title = TermuxConstants.TERMUX_APP_NAME + " Plugin Execution Command Error";
|
||||||
|
|
||||||
|
StringBuilder reportString = new StringBuilder();
|
||||||
|
|
||||||
|
reportString.append(ExecutionCommand.getExecutionCommandMarkdownString(executionCommand));
|
||||||
|
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true));
|
||||||
|
reportString.append("\n\n").append(TermuxUtils.getDeviceInfoMarkdownString(context));
|
||||||
|
|
||||||
|
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.PLUGIN_EXECUTION_COMMAND, logTag, title, null, reportString.toString(), null,true));
|
||||||
|
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
|
||||||
|
// Setup the notification channel if not already set up
|
||||||
|
setupPluginCommandErrorsNotificationChannel(context);
|
||||||
|
|
||||||
|
// Use markdown in notification
|
||||||
|
CharSequence notificationText = MarkdownUtils.getSpannedMarkdownText(context, executionCommand.errmsg);
|
||||||
|
//CharSequence notificationText = executionCommand.errmsg;
|
||||||
|
|
||||||
|
// Build the notification
|
||||||
|
Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(context, title, notificationText, notificationText, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
|
||||||
|
if (builder == null) return;
|
||||||
|
|
||||||
|
// Send the notification
|
||||||
|
int nextNotificationId = NotificationUtils.getNextNotificationId(context);
|
||||||
|
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
|
||||||
|
if (notificationManager != null)
|
||||||
|
notificationManager.notify(nextNotificationId, builder.build());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send {@link ExecutionCommand} result {@link PendingIntent} in the
|
||||||
|
* {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE} bundle.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} that will be used to send result intent to the {@link PendingIntent} creator.
|
||||||
|
* @param logTag The log tag to use for logging.
|
||||||
|
* @param label The label of {@link ExecutionCommand}.
|
||||||
|
* @param stdout The stdout of {@link ExecutionCommand}.
|
||||||
|
* @param stderr The stderr of {@link ExecutionCommand}.
|
||||||
|
* @param exitCode The exitCode of {@link ExecutionCommand}.
|
||||||
|
* @param errCode The errCode of {@link ExecutionCommand}.
|
||||||
|
* @param errmsg The errmsg of {@link ExecutionCommand}.
|
||||||
|
* @param pluginPendingIntent The pluginPendingIntent of {@link ExecutionCommand}.
|
||||||
|
* @return Returns {@code true} if pluginPendingIntent was successfully send, otherwise [@code false}.
|
||||||
|
*/
|
||||||
|
public static boolean sendPluginExecutionCommandResultPendingIntent(Context context, String logTag, String label, String stdout, String stderr, Integer exitCode, Integer errCode, String errmsg, PendingIntent pluginPendingIntent) {
|
||||||
|
if (context == null || pluginPendingIntent == null) return false;
|
||||||
|
|
||||||
|
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||||
|
|
||||||
|
Logger.logDebug(logTag, "Sending execution result for Execution Command \"" + label + "\" to " + pluginPendingIntent.getCreatorPackage());
|
||||||
|
|
||||||
|
String truncatedStdout = null;
|
||||||
|
String truncatedStderr = null;
|
||||||
|
|
||||||
|
String stdoutOriginalLength = (stdout == null) ? null: String.valueOf(stdout.length());
|
||||||
|
String stderrOriginalLength = (stderr == null) ? null: String.valueOf(stderr.length());
|
||||||
|
|
||||||
|
// Truncate stdout and stdout to max TRANSACTION_SIZE_LIMIT_IN_BYTES
|
||||||
|
if (stderr == null || stderr.isEmpty()) {
|
||||||
|
truncatedStdout = DataUtils.getTruncatedCommandOutput(stdout, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false);
|
||||||
|
} else if (stdout == null || stdout.isEmpty()) {
|
||||||
|
truncatedStderr = DataUtils.getTruncatedCommandOutput(stderr, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false);
|
||||||
|
} else {
|
||||||
|
truncatedStdout = DataUtils.getTruncatedCommandOutput(stdout, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 2, false, false, false);
|
||||||
|
truncatedStderr = DataUtils.getTruncatedCommandOutput(stderr, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 2, false, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (truncatedStdout != null && truncatedStdout.length() < stdout.length()) {
|
||||||
|
Logger.logWarn(logTag, "Execution Result for Execution Command \"" + label + "\" stdout length truncated from " + stdoutOriginalLength + " to " + truncatedStdout.length());
|
||||||
|
stdout = truncatedStdout;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (truncatedStderr != null && truncatedStderr.length() < stderr.length()) {
|
||||||
|
Logger.logWarn(logTag, "Execution Result for Execution Command \"" + label + "\" stderr length truncated from " + stderrOriginalLength + " to " + truncatedStderr.length());
|
||||||
|
stderr = truncatedStderr;
|
||||||
|
}
|
||||||
|
|
||||||
|
String errmsgOriginalLength = (errmsg == null) ? null: String.valueOf(errmsg.length());
|
||||||
|
|
||||||
|
// Truncate errmsg to max TRANSACTION_SIZE_LIMIT_IN_BYTES / 4
|
||||||
|
// trim from end to preserve start of stacktraces
|
||||||
|
String truncatedErrmsg = DataUtils.getTruncatedCommandOutput(errmsg, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 4, true, false, false);
|
||||||
|
if (truncatedErrmsg != null && truncatedErrmsg.length() < errmsg.length()) {
|
||||||
|
Logger.logWarn(logTag, "Execution Result for Execution Command \"" + label + "\" errmsg length truncated from " + errmsgOriginalLength + " to " + truncatedErrmsg.length());
|
||||||
|
errmsg = truncatedErrmsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
final Bundle resultBundle = new Bundle();
|
||||||
|
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT, stdout);
|
||||||
|
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH, stdoutOriginalLength);
|
||||||
|
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR, stderr);
|
||||||
|
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH, stderrOriginalLength);
|
||||||
|
if (exitCode != null) resultBundle.putInt(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE, exitCode);
|
||||||
|
if (errCode != null) resultBundle.putInt(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERR, errCode);
|
||||||
|
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG, errmsg);
|
||||||
|
|
||||||
|
Intent resultIntent = new Intent();
|
||||||
|
resultIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE, resultBundle);
|
||||||
|
|
||||||
|
try {
|
||||||
|
pluginPendingIntent.send(context, Activity.RESULT_OK, resultIntent);
|
||||||
|
} catch (PendingIntent.CanceledException e) {
|
||||||
|
// The caller doesn't want the result? That's fine, just ignore
|
||||||
|
Logger.logDebug(logTag, "The Execution Command \"" + label + "\" creator " + pluginPendingIntent.getCreatorPackage() + " does not want the results anymore");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
|
||||||
|
* and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
* @param title The title for the notification.
|
||||||
|
* @param notificationText The second line text of the notification.
|
||||||
|
* @param notificationBigText The full text of the notification that may optionally be styled.
|
||||||
|
* @param pendingIntent The {@link PendingIntent} which should be sent when notification is clicked.
|
||||||
|
* @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}.
|
||||||
|
* @return Returns the {@link Notification.Builder}.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static Notification.Builder getPluginCommandErrorsNotificationBuilder(final Context context, final CharSequence title, final CharSequence notificationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) {
|
||||||
|
|
||||||
|
Notification.Builder builder = NotificationUtils.geNotificationBuilder(context,
|
||||||
|
TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_HIGH,
|
||||||
|
title, notificationText, notificationBigText, pendingIntent, notificationMode);
|
||||||
|
|
||||||
|
if (builder == null) return null;
|
||||||
|
|
||||||
|
// Enable timestamp
|
||||||
|
builder.setShowWhen(true);
|
||||||
|
|
||||||
|
// Set notification icon
|
||||||
|
builder.setSmallIcon(R.drawable.ic_error_notification);
|
||||||
|
|
||||||
|
// Set background color for small notification icon
|
||||||
|
builder.setColor(0xFF607D8B);
|
||||||
|
|
||||||
|
// Dismiss on click
|
||||||
|
builder.setAutoCancel(true);
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup the notification channel for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID} and
|
||||||
|
* {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
*/
|
||||||
|
public static void setupPluginCommandErrorsNotificationChannel(final Context context) {
|
||||||
|
NotificationUtils.setupNotificationChannel(context, TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID,
|
||||||
|
TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if {@link TermuxConstants#PROP_ALLOW_EXTERNAL_APPS} property is not set to "true".
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} to get error string.
|
||||||
|
* @return Returns the {@code errmsg} if policy is violated, otherwise {@code null}.
|
||||||
|
*/
|
||||||
|
public static String checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(final Context context) {
|
||||||
|
String errmsg = null;
|
||||||
|
if (!SharedProperties.isPropertyValueTrue(context, TermuxPropertyConstants.getTermuxPropertiesFile(), TermuxConstants.PROP_ALLOW_EXTERNAL_APPS)) {
|
||||||
|
errmsg = context.getString(R.string.error_run_command_service_allow_external_apps_ungranted);
|
||||||
|
}
|
||||||
|
|
||||||
|
return errmsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ import android.provider.DocumentsProvider;
|
|||||||
import android.webkit.MimeTypeMap;
|
import android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.app.TermuxService;
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
@@ -22,7 +22,7 @@ import java.util.LinkedList;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* A document provider for the Storage Access Framework which exposes the files in the
|
* A document provider for the Storage Access Framework which exposes the files in the
|
||||||
* $HOME/ folder to other apps.
|
* $HOME/ directory to other apps.
|
||||||
* <p/>
|
* <p/>
|
||||||
* Note that this replaces providing an activity matching the ACTION_GET_CONTENT intent:
|
* Note that this replaces providing an activity matching the ACTION_GET_CONTENT intent:
|
||||||
* <p/>
|
* <p/>
|
||||||
@@ -35,7 +35,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
|||||||
|
|
||||||
private static final String ALL_MIME_TYPES = "*/*";
|
private static final String ALL_MIME_TYPES = "*/*";
|
||||||
|
|
||||||
private static final File BASE_DIR = new File(TermuxService.HOME_PATH);
|
private static final File BASE_DIR = TermuxConstants.TERMUX_HOME_DIR;
|
||||||
|
|
||||||
|
|
||||||
// The default columns to return information about a root if no specific
|
// The default columns to return information about a root if no specific
|
||||||
@@ -63,9 +63,9 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
|
public Cursor queryRoots(String[] projection) {
|
||||||
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION);
|
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION);
|
||||||
@SuppressWarnings("ConstantConditions") final String applicationName = getContext().getString(R.string.application_name);
|
final String applicationName = getContext().getString(R.string.application_name);
|
||||||
|
|
||||||
final MatrixCursor.RowBuilder row = result.newRow();
|
final MatrixCursor.RowBuilder row = result.newRow();
|
||||||
row.add(Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR));
|
row.add(Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR));
|
||||||
@@ -75,7 +75,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
|||||||
row.add(Root.COLUMN_TITLE, applicationName);
|
row.add(Root.COLUMN_TITLE, applicationName);
|
||||||
row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES);
|
row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES);
|
||||||
row.add(Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.getFreeSpace());
|
row.add(Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.getFreeSpace());
|
||||||
row.add(Root.COLUMN_ICON, R.drawable.ic_launcher);
|
row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,9 +91,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
|||||||
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
|
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
|
||||||
final File parent = getFileForDocId(parentDocumentId);
|
final File parent = getFileForDocId(parentDocumentId);
|
||||||
for (File file : parent.listFiles()) {
|
for (File file : parent.listFiles()) {
|
||||||
if (!file.getName().startsWith(".")) {
|
includeFile(result, null, file);
|
||||||
includeFile(result, null, file);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -169,16 +167,15 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
|||||||
final int MAX_SEARCH_RESULTS = 50;
|
final int MAX_SEARCH_RESULTS = 50;
|
||||||
while (!pending.isEmpty() && result.getCount() < MAX_SEARCH_RESULTS) {
|
while (!pending.isEmpty() && result.getCount() < MAX_SEARCH_RESULTS) {
|
||||||
final File file = pending.removeFirst();
|
final File file = pending.removeFirst();
|
||||||
// Avoid folders outside the $HOME folders linked in to symlinks (to avoid e.g. search
|
// Avoid directories outside the $HOME directory linked with symlinks (to avoid e.g. search
|
||||||
// through the whole SD card).
|
// through the whole SD card).
|
||||||
boolean isInsideHome;
|
boolean isInsideHome;
|
||||||
try {
|
try {
|
||||||
isInsideHome = file.getCanonicalPath().startsWith(TermuxService.HOME_PATH);
|
isInsideHome = file.getCanonicalPath().startsWith(TermuxConstants.TERMUX_HOME_DIR_PATH);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
isInsideHome = true;
|
isInsideHome = true;
|
||||||
}
|
}
|
||||||
final boolean isHidden = file.getName().startsWith(".");
|
if (isInsideHome) {
|
||||||
if (isInsideHome && !isHidden) {
|
|
||||||
if (file.isDirectory()) {
|
if (file.isDirectory()) {
|
||||||
Collections.addAll(pending, file.listFiles());
|
Collections.addAll(pending, file.listFiles());
|
||||||
} else {
|
} else {
|
||||||
@@ -265,7 +262,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
|||||||
row.add(Document.COLUMN_MIME_TYPE, mimeType);
|
row.add(Document.COLUMN_MIME_TYPE, mimeType);
|
||||||
row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
|
row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
|
||||||
row.add(Document.COLUMN_FLAGS, flags);
|
row.add(Document.COLUMN_FLAGS, flags);
|
||||||
row.add(Document.COLUMN_ICON, R.drawable.ic_launcher);
|
row.add(Document.COLUMN_ICON, R.mipmap.ic_launcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import android.content.Intent;
|
|||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.provider.OpenableColumns;
|
import android.provider.OpenableColumns;
|
||||||
import android.util.Log;
|
|
||||||
import android.util.Patterns;
|
import android.util.Patterns;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.app.DialogUtils;
|
import com.termux.shared.interact.DialogUtils;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||||
import com.termux.app.TermuxService;
|
import com.termux.app.TermuxService;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -25,9 +27,9 @@ import java.util.regex.Pattern;
|
|||||||
|
|
||||||
public class TermuxFileReceiverActivity extends Activity {
|
public class TermuxFileReceiverActivity extends Activity {
|
||||||
|
|
||||||
static final String TERMUX_RECEIVEDIR = TermuxService.FILES_PATH + "/home/downloads";
|
static final String TERMUX_RECEIVEDIR = TermuxConstants.TERMUX_FILES_DIR_PATH + "/home/downloads";
|
||||||
static final String EDITOR_PROGRAM = TermuxService.HOME_PATH + "/bin/termux-file-editor";
|
static final String EDITOR_PROGRAM = TermuxConstants.TERMUX_HOME_DIR_PATH + "/bin/termux-file-editor";
|
||||||
static final String URL_OPENER_PROGRAM = TermuxService.HOME_PATH + "/bin/termux-url-opener";
|
static final String URL_OPENER_PROGRAM = TermuxConstants.TERMUX_HOME_DIR_PATH + "/bin/termux-url-opener";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the activity should be finished when the name input dialog is dismissed. This is disabled
|
* If the activity should be finished when the name input dialog is dismissed. This is disabled
|
||||||
@@ -37,6 +39,8 @@ public class TermuxFileReceiverActivity extends Activity {
|
|||||||
*/
|
*/
|
||||||
boolean mFinishOnDismissNameDialog = true;
|
boolean mFinishOnDismissNameDialog = true;
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "TermuxFileReceiverActivity";
|
||||||
|
|
||||||
static boolean isSharedTextAnUrl(String sharedText) {
|
static boolean isSharedTextAnUrl(String sharedText) {
|
||||||
return Patterns.WEB_URL.matcher(sharedText).matches()
|
return Patterns.WEB_URL.matcher(sharedText).matches()
|
||||||
|| Pattern.matches("magnet:\\?xt=urn:btih:.*?", sharedText);
|
|| Pattern.matches("magnet:\\?xt=urn:btih:.*?", sharedText);
|
||||||
@@ -109,12 +113,12 @@ public class TermuxFileReceiverActivity extends Activity {
|
|||||||
promptNameAndSave(in, attachmentFileName);
|
promptNameAndSave(in, attachmentFileName);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
showErrorDialogAndQuit("Unable to handle shared content:\n\n" + e.getMessage());
|
showErrorDialogAndQuit("Unable to handle shared content:\n\n" + e.getMessage());
|
||||||
Log.e("termux", "handleContentUri(uri=" + uri + ") failed", e);
|
Logger.logStackTraceWithMessage(LOG_TAG, "handleContentUri(uri=" + uri + ") failed", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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, R.string.file_received_edit_button, text -> {
|
DialogUtils.textInput(this, R.string.title_file_received, attachmentFileName, R.string.action_file_received_edit, text -> {
|
||||||
File outFile = saveStreamWithName(in, text);
|
File outFile = saveStreamWithName(in, text);
|
||||||
if (outFile == null) return;
|
if (outFile == null) return;
|
||||||
|
|
||||||
@@ -131,17 +135,17 @@ public class TermuxFileReceiverActivity extends Activity {
|
|||||||
|
|
||||||
final Uri scriptUri = new Uri.Builder().scheme("file").path(EDITOR_PROGRAM).build();
|
final Uri scriptUri = new Uri.Builder().scheme("file").path(EDITOR_PROGRAM).build();
|
||||||
|
|
||||||
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE, scriptUri);
|
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, scriptUri);
|
||||||
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
||||||
executeIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()});
|
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()});
|
||||||
startService(executeIntent);
|
startService(executeIntent);
|
||||||
finish();
|
finish();
|
||||||
},
|
},
|
||||||
R.string.file_received_open_folder_button, text -> {
|
R.string.action_file_received_open_directory, text -> {
|
||||||
if (saveStreamWithName(in, text) == null) return;
|
if (saveStreamWithName(in, text) == null) return;
|
||||||
|
|
||||||
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE);
|
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE);
|
||||||
executeIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, TERMUX_RECEIVEDIR);
|
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, TERMUX_RECEIVEDIR);
|
||||||
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
||||||
startService(executeIntent);
|
startService(executeIntent);
|
||||||
finish();
|
finish();
|
||||||
@@ -169,7 +173,7 @@ public class TermuxFileReceiverActivity extends Activity {
|
|||||||
return outFile;
|
return outFile;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
showErrorDialogAndQuit("Error saving file:\n\n" + e);
|
showErrorDialogAndQuit("Error saving file:\n\n" + e);
|
||||||
Log.e("termux", "Error saving file", e);
|
Logger.logStackTraceWithMessage(LOG_TAG, "Error saving file", e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,9 +192,9 @@ public class TermuxFileReceiverActivity extends Activity {
|
|||||||
|
|
||||||
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();
|
||||||
|
|
||||||
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE, urlOpenerProgramUri);
|
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, urlOpenerProgramUri);
|
||||||
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
||||||
executeIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, new String[]{url});
|
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, new String[]{url});
|
||||||
startService(executeIntent);
|
startService(executeIntent);
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|||||||
5
app/src/main/res/drawable/ic_copy.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/>
|
||||||
|
</vector>
|
||||||
37
app/src/main/res/drawable/ic_error_notification.xml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<!--
|
||||||
|
Updated notification icon compliant with system icons guidelines
|
||||||
|
https://material.io/design/iconography/system-icons.html
|
||||||
|
-->
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0,0h24v24h-24z"/>
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:pathData="M5,4H2L8,12L2,20H5L11,12L5,4Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:pathData="M19.59,14
|
||||||
|
l-2.09,2.09
|
||||||
|
L15.41,14
|
||||||
|
L14,15.41
|
||||||
|
l2.09,2.09
|
||||||
|
L14,19.59
|
||||||
|
L15.41,21
|
||||||
|
l2.09,-2.08
|
||||||
|
L19.59,21
|
||||||
|
L21,19.59
|
||||||
|
l-2.08,-2.09
|
||||||
|
L21,15.41
|
||||||
|
L19.59,14
|
||||||
|
z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:height="108dp"
|
|
||||||
android:width="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:fillColor="#000000"
|
|
||||||
android:pathData="M18,54
|
|
||||||
A36,36 0 1,1 90,54
|
|
||||||
A36,36 0 1,1 18,54 Z" />
|
|
||||||
|
|
||||||
<!-- Keep in sync with adaptive ic_foreground.xml: -->
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M34,38
|
|
||||||
h6
|
|
||||||
l12,16
|
|
||||||
l-12,16
|
|
||||||
h-6
|
|
||||||
l12,-16
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M56,66
|
|
||||||
h18
|
|
||||||
v4
|
|
||||||
h-18
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</vector>
|
|
||||||
5
app/src/main/res/drawable/ic_settings.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#FF000000"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
|
||||||
|
</vector>
|
||||||
5
app/src/main/res/drawable/ic_share.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
|
||||||
|
</vector>
|
||||||
21
app/src/main/res/layout/activity_report.xml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<include
|
||||||
|
layout="@layout/partial_toolbar"
|
||||||
|
android:id="@+id/partial_toolbar"/>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:overScrollMode="never"
|
||||||
|
android:paddingTop="@dimen/content_padding"
|
||||||
|
android:paddingBottom="36dip" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:fillViewport="true"
|
||||||
|
android:paddingLeft="16dip"
|
||||||
|
android:paddingRight="16dip"
|
||||||
|
android:scrollbarStyle="outsideInset">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/code_text_view"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@color/background_markdown_code_block"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:lineSpacingExtra="2dip"
|
||||||
|
android:paddingLeft="16dip"
|
||||||
|
android:paddingTop="8dip"
|
||||||
|
android:paddingRight="16dip"
|
||||||
|
android:paddingBottom="8dip"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
</HorizontalScrollView>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/default_text_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="16dip"
|
||||||
|
android:layout_marginRight="16dip"
|
||||||
|
android:breakStrategy="simple"
|
||||||
|
android:hyphenationFrequency="none"
|
||||||
|
android:lineSpacingExtra="2dip"
|
||||||
|
android:paddingTop="8dip"
|
||||||
|
android:paddingBottom="8dip"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
|
android:textColor="#000"
|
||||||
|
android:textSize="12sp" />
|
||||||
9
app/src/main/res/layout/activity_settings.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/settings"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
</LinearLayout>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
android:id="@+id/drawer_layout"
|
android:id="@+id/drawer_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_alignParentTop="true"
|
android:layout_alignParentTop="true"
|
||||||
android:layout_above="@+id/viewpager"
|
android:layout_above="@+id/terminal_toolbar_view_pager"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<com.termux.view.TerminalView
|
<com.termux.view.TerminalView
|
||||||
@@ -19,7 +19,9 @@
|
|||||||
android:layout_marginLeft="3dp"
|
android:layout_marginLeft="3dp"
|
||||||
android:focusableInTouchMode="true"
|
android:focusableInTouchMode="true"
|
||||||
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
|
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
|
||||||
android:scrollbars="vertical" />
|
android:scrollbars="vertical"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:autofillHints="password" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/left_drawer"
|
android:id="@+id/left_drawer"
|
||||||
@@ -34,7 +36,7 @@
|
|||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<ListView
|
<ListView
|
||||||
android:id="@+id/left_drawer_list"
|
android:id="@+id/terminal_sessions_list"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_gravity="top"
|
android:layout_gravity="top"
|
||||||
@@ -54,7 +56,7 @@
|
|||||||
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/toggle_soft_keyboard" />
|
android:text="@string/action_toggle_soft_keyboard" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/new_session_button"
|
android:id="@+id/new_session_button"
|
||||||
@@ -62,14 +64,14 @@
|
|||||||
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/action_new_session" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</androidx.drawerlayout.widget.DrawerLayout>
|
</androidx.drawerlayout.widget.DrawerLayout>
|
||||||
|
|
||||||
<androidx.viewpager.widget.ViewPager
|
<androidx.viewpager.widget.ViewPager
|
||||||
android:id="@+id/viewpager"
|
android:id="@+id/terminal_toolbar_view_pager"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="37.5dp"
|
android:layout_height="37.5dp"
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:id="@+id/row_line"
|
android:id="@+id/session_title"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="?android:attr/listPreferredItemHeight"
|
android:layout_height="?android:attr/listPreferredItemHeight"
|
||||||
android:background="@drawable/selected_session_background"
|
android:background="@drawable/session_background_selected"
|
||||||
android:ellipsize="marquee"
|
android:ellipsize="marquee"
|
||||||
android:gravity="start|center_vertical"
|
android:gravity="start|center_vertical"
|
||||||
android:padding="6dip"
|
android:padding="6dip"
|
||||||
android:textSize="14sp" />
|
android:textSize="14sp" />
|
||||||
22
app/src/main/res/layout/partial_toolbar.xml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/toolbar_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:background="?attr/colorPrimaryDark"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:minHeight="?attr/actionBarSize"
|
||||||
|
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
|
||||||
|
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
|
||||||
|
app:titleTextAppearance="@style/Toolbar.Title">
|
||||||
|
|
||||||
|
</androidx.appcompat.widget.Toolbar>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<com.termux.app.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
|
<com.termux.app.terminal.io.extrakeys.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:id="@+id/extra_keys"
|
android:id="@+id/terminal_toolbar_extra_keys"
|
||||||
style="?android:attr/buttonBarStyle"
|
style="?android:attr/buttonBarStyle"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<EditText xmlns:android="http://schemas.android.com/apk/res/android"
|
<EditText xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/text_input"
|
android:id="@+id/terminal_toolbar_text_input"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:imeOptions="actionSend|flagNoFullscreen"
|
android:imeOptions="actionSend|flagNoFullscreen"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:inputType="text"
|
android:inputType="text"
|
||||||
|
android:importantForAutofill="no"
|
||||||
android:textColor="@android:color/white"
|
android:textColor="@android:color/white"
|
||||||
android:textColorHighlight="@android:color/darker_gray"
|
android:textColorHighlight="@android:color/darker_gray"
|
||||||
android:paddingTop="0dp"
|
android:paddingTop="0dp"
|
||||||
15
app/src/main/res/menu/menu_report.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_item_share_report"
|
||||||
|
android:icon="@drawable/ic_share"
|
||||||
|
android:title="@string/action_share"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_item_copy_report"
|
||||||
|
android:icon="@drawable/ic_copy"
|
||||||
|
android:title="@string/action_copy"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
</menu>
|
||||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@android:color/black"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
3
app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
</resources>
|
||||||
10
app/src/main/res/values/dimens.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Default screen margins, per the Android Design guidelines. -->
|
||||||
|
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||||
|
<dimen name="activity_vertical_margin">16dp</dimen>
|
||||||
|
|
||||||
|
<dimen name="content_padding">8dip</dimen>
|
||||||
|
<dimen name="content_padding_double">16dip</dimen>
|
||||||
|
<dimen name="content_padding_half">4dip</dimen>
|
||||||
|
</resources>
|
||||||
@@ -1,53 +1,158 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
|
||||||
<string name="application_name">Termux</string>
|
|
||||||
<string name="shared_user_label">Termux user</string>
|
|
||||||
<string name="run_command_permission_label">Run commands in Termux environment</string>
|
|
||||||
<string name="run_command_permission_description">execute arbitrary commands within Termux environment</string>
|
|
||||||
<string name="new_session">New session</string>
|
|
||||||
<string name="new_session_failsafe">Failsafe</string>
|
|
||||||
<string name="toggle_soft_keyboard">Keyboard</string>
|
|
||||||
<string name="reset_terminal">Reset</string>
|
|
||||||
<string name="style_terminal">Style</string>
|
|
||||||
<string name="share_transcript_title">Terminal transcript</string>
|
|
||||||
<string name="help">Help</string>
|
|
||||||
<string name="toggle_keep_screen_on">Keep screen on</string>
|
|
||||||
|
|
||||||
<string name="bootstrap_installer_body">Installing…</string>
|
<!DOCTYPE resources [
|
||||||
<string name="bootstrap_error_title">Unable to install</string>
|
<!ENTITY TERMUX_PACKAGE_NAME "com.termux">
|
||||||
<string name="bootstrap_error_body">Termux was unable to install the bootstrap packages.</string>
|
<!ENTITY TERMUX_APP_NAME "Termux">
|
||||||
|
<!ENTITY TERMUX_API_APP_NAME "Termux:API">
|
||||||
|
<!ENTITY TERMUX_BOOT_APP_NAME "Termux:Boot">
|
||||||
|
<!ENTITY TERMUX_FLOAT_APP_NAME "Termux:Float">
|
||||||
|
<!ENTITY TERMUX_STYLING_APP_NAME "Termux:Styling">
|
||||||
|
<!ENTITY TERMUX_TASKER_APP_NAME "Termux:Tasker">
|
||||||
|
<!ENTITY TERMUX_WIDGET_APP_NAME "Termux:Widget">
|
||||||
|
<!ENTITY TERMUX_PROPERTIES_PRIMARY_PATH_SHORT "~/.termux/termux.properties">
|
||||||
|
]>
|
||||||
|
|
||||||
|
<resources>
|
||||||
|
<string name="application_name">&TERMUX_APP_NAME;</string>
|
||||||
|
<string name="shared_user_label">&TERMUX_APP_NAME; user</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Termux RUN_COMMAND permission -->
|
||||||
|
<string name="permission_run_command_label">Run commands in &TERMUX_APP_NAME; environment</string>
|
||||||
|
<string name="permission_run_command_description">execute arbitrary commands within &TERMUX_APP_NAME;
|
||||||
|
environment</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Termux Bootstrap Packages Installation -->
|
||||||
|
<string name="bootstrap_installer_body">Installing bootstrap packages…</string>
|
||||||
|
<string name="bootstrap_error_title">Unable to install bootstrap</string>
|
||||||
|
<string name="bootstrap_error_body">&TERMUX_APP_NAME; was unable to install the bootstrap packages.</string>
|
||||||
<string name="bootstrap_error_abort">Abort</string>
|
<string name="bootstrap_error_abort">Abort</string>
|
||||||
<string name="bootstrap_error_try_again">Try again</string>
|
<string name="bootstrap_error_try_again">Try again</string>
|
||||||
<string name="bootstrap_error_not_primary_user_message">Termux can only be installed on the primary user account.</string>
|
<string name="bootstrap_error_not_primary_user_message">&TERMUX_APP_NAME; can only be run as the primary user.\nBootstrap binaries compiled for &TERMUX_APP_NAME; have hardcoded $PREFIX path and cannot be installed under any path other than \"%1$s\".</string>
|
||||||
|
|
||||||
<string name="max_terminals_reached_title">Max terminals reached</string>
|
|
||||||
<string name="max_terminals_reached_message">Close down existing ones before creating new.</string>
|
|
||||||
|
|
||||||
<string name="reset_toast_notification">Terminal reset.</string>
|
|
||||||
|
|
||||||
<string name="select_url">Select URL</string>
|
<!-- Terminal Sidebar and Shortcuts -->
|
||||||
<string name="select_url_dialog_title">Click URL to copy or long press to open</string>
|
<string name="action_new_session">New session</string>
|
||||||
<string name="select_all_and_share">Share transcript</string>
|
<string name="action_new_session_failsafe">Failsafe</string>
|
||||||
<string name="select_url_no_found">No URL found in the terminal.</string>
|
<string name="title_max_terminals_reached">Max terminals reached</string>
|
||||||
<string name="select_url_copied_to_clipboard">URL copied to clipboard</string>
|
<string name="msg_max_terminals_reached">Close down existing ones before creating new.</string>
|
||||||
<string name="share_transcript_chooser_title">Send text to:</string>
|
|
||||||
|
|
||||||
<string name="kill_process">Kill process (%d)</string>
|
<string name="title_rename_session">Set session name</string>
|
||||||
<string name="confirm_kill_process">Really kill this session?</string>
|
<string name="action_rename_session_confirm">Set</string>
|
||||||
|
<string name="title_create_named_session">New named session</string>
|
||||||
|
<string name="action_create_named_session_confirm">Create</string>
|
||||||
|
|
||||||
<string name="session_rename_title">Set session name</string>
|
<string name="action_toggle_soft_keyboard">Keyboard</string>
|
||||||
<string name="session_rename_positive_button">Set</string>
|
|
||||||
<string name="session_new_named_title">New named session</string>
|
|
||||||
<string name="session_new_named_positive_button">Create</string>
|
|
||||||
|
|
||||||
<string name="styling_not_installed">The Termux:Style add-on is not installed.</string>
|
<string name="msg_enabling_terminal_toolbar">Enabling Terminal Toolbar</string>
|
||||||
<string name="styling_install">Install</string>
|
<string name="msg_disabling_terminal_toolbar">Disabling Terminal Toolbar</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Terminal Popup -->
|
||||||
|
<string name="action_select_url">Select URL</string>
|
||||||
|
<string name="title_select_url_dialog">Click URL to copy or long press to open</string>
|
||||||
|
<string name="title_select_url_none_found">No URL found in the terminal.</string>
|
||||||
|
<string name="msg_select_url_copied_to_clipboard">URL copied to clipboard</string>
|
||||||
|
|
||||||
|
<string name="action_share_transcript">Share transcript</string>
|
||||||
|
<string name="title_share_transcript">Terminal transcript</string>
|
||||||
|
<string name="title_share_transcript_with">Send transcript to:</string>
|
||||||
|
|
||||||
|
<string name="action_autofill_password">Autofill password</string>
|
||||||
|
|
||||||
|
<string name="action_reset_terminal">Reset</string>
|
||||||
|
<string name="msg_terminal_reset">Terminal reset.</string>
|
||||||
|
|
||||||
|
<string name="action_kill_process">Kill process (%d)</string>
|
||||||
|
<string name="title_confirm_kill_process">Really kill this session?</string>
|
||||||
|
|
||||||
|
<string name="action_style_terminal">Style</string>
|
||||||
|
<string name="action_toggle_keep_screen_on">Keep screen on</string>
|
||||||
|
<string name="action_open_help">Help</string>
|
||||||
|
<string name="action_open_settings">Settings</string>
|
||||||
|
<string name="action_report_issue">Report Issue</string>
|
||||||
|
|
||||||
|
<string name="error_styling_not_installed">The &TERMUX_STYLING_APP_NAME; Plugin App is not installed.</string>
|
||||||
|
<string name="action_styling_install">Install</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Termux Notifications -->
|
||||||
<string name="notification_action_exit">Exit</string>
|
<string name="notification_action_exit">Exit</string>
|
||||||
<string name="notification_action_wake_lock">Acquire wakelock</string>
|
<string name="notification_action_wake_lock">Acquire wakelock</string>
|
||||||
<string name="notification_action_wake_unlock">Release wakelock</string>
|
<string name="notification_action_wake_unlock">Release wakelock</string>
|
||||||
|
|
||||||
<string name="file_received_title">Save file in ~/downloads/</string>
|
|
||||||
<string name="file_received_edit_button">Edit</string>
|
|
||||||
<string name="file_received_open_folder_button">Open folder</string>
|
<!-- Termux RunCommandService -->
|
||||||
|
<string name="error_run_command_service_invalid_intent_action">Invalid intent action to RunCommandService: `%1$s`</string>
|
||||||
|
<string name="error_run_command_service_mandatory_extra_missing">Mandatory extra missing to RunCommandService: \"%1$s\"</string>
|
||||||
|
<string name="error_run_command_service_allow_external_apps_ungranted">RunCommandService require `allow-external-apps` property to be set to `true` in `&TERMUX_PROPERTIES_PRIMARY_PATH_SHORT;` file.</string>
|
||||||
|
<string name="error_run_command_service_api_help">Visit %1$s for more info on RUN_COMMAND Intent usage.</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Termux Execution Commands -->
|
||||||
|
<string name="msg_executable_absolute_path">Executable Absolute Path: \"%1$s\"</string>
|
||||||
|
<string name="msg_working_directory_absolute_path">Working Directory Absolute Path: \"%1$s\"</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Termux Report And ShareUtils -->
|
||||||
|
<string name="action_copy">Copy</string>
|
||||||
|
<string name="action_share">Share</string>
|
||||||
|
|
||||||
|
<string name="title_share_with">Share With</string>
|
||||||
|
<string name="title_report_text">Report Text</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Termux File Receiver -->
|
||||||
|
<string name="title_file_received">Save file in ~/downloads/</string>
|
||||||
|
<string name="action_file_received_edit">Edit</string>
|
||||||
|
<string name="action_file_received_open_directory">Open directory</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Termux Settings -->
|
||||||
|
<string name="title_activity_termux_settings">&TERMUX_APP_NAME; Settings</string>
|
||||||
|
|
||||||
|
<!-- Debugging Preferences -->
|
||||||
|
<string name="debugging_preferences">Debugging</string>
|
||||||
|
|
||||||
|
<!-- Logging Category -->
|
||||||
|
<string name="logging_header">Logging</string>
|
||||||
|
|
||||||
|
<!-- Terminal View Key Logging -->
|
||||||
|
<string name="terminal_view_key_logging_title">Terminal View Key Logging</string>
|
||||||
|
<string name="terminal_view_key_logging_off">Logs will not have entries for terminal view keys. (Default)</string>
|
||||||
|
<string name="terminal_view_key_logging_on">Logcat logs will have entries for terminal view keys. These are very verbose and should be disabled under normal circumstances or will cause performance issues.</string>
|
||||||
|
|
||||||
|
<!-- Plugin Error Notifications -->
|
||||||
|
<string name="plugin_error_notifications_title">Plugin Error Notifications</string>
|
||||||
|
<string name="plugin_error_notifications_off">Disable flashes and notifications for plugin errors.</string>
|
||||||
|
<string name="plugin_error_notifications_on">Show flashes and notifications for plugin errors. (Default)</string>
|
||||||
|
|
||||||
|
<!-- Crash Report Notifications -->
|
||||||
|
<string name="crash_report_notifications_title">Crash Report Notifications</string>
|
||||||
|
<string name="crash_report_notifications_off">Disable notifications for crash reports.</string>
|
||||||
|
<string name="crash_report_notifications_on">Show notifications for crash reports. (Default)</string>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Terminal IO Preferences -->
|
||||||
|
<string name="terminal_io_preferences">Terminal I/O</string>
|
||||||
|
|
||||||
|
<!-- Keyboard Category -->
|
||||||
|
<string name="keyboard_header">Keyboard</string>
|
||||||
|
|
||||||
|
<!-- Soft Keyboard -->
|
||||||
|
<string name="soft_keyboard_title">Soft Keyboard</string>
|
||||||
|
<string name="soft_keyboard_off">Soft keyboard will be disabled.</string>
|
||||||
|
<string name="soft_keyboard_on">Soft keyboard will be enabled. (Default)</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -21,10 +21,6 @@
|
|||||||
<item name="android:windowAllowEnterTransitionOverlap">true</item>
|
<item name="android:windowAllowEnterTransitionOverlap">true</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">
|
|
||||||
<!-- Seen in buttons on alert dialog: -->
|
|
||||||
<item name="android:colorAccent">#212121</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- See https://developer.android.com/training/material/theme.html for how to customize the Material theme. -->
|
<!-- See https://developer.android.com/training/material/theme.html for how to customize the Material theme. -->
|
||||||
<!-- NOTE: Cannot use "Light." since it hides the terminal scrollbar on the default black background. -->
|
<!-- NOTE: Cannot use "Light." since it hides the terminal scrollbar on the default black background. -->
|
||||||
@@ -46,4 +42,20 @@
|
|||||||
<item name="android:windowAllowReturnTransitionOverlap">true</item>
|
<item name="android:windowAllowReturnTransitionOverlap">true</item>
|
||||||
<item name="android:windowAllowEnterTransitionOverlap">true</item>
|
<item name="android:windowAllowEnterTransitionOverlap">true</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<style name="Theme.AppCompat.TermuxReportActivity" parent="Theme.AppCompat.Light.NoActionBar">
|
||||||
|
<item name="colorPrimaryDark">#FF0000</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Toolbar.Title" parent="TextAppearance.Widget.AppCompat.Toolbar.Title">
|
||||||
|
<item name="android:textSize">14sp</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">
|
||||||
|
<!-- Seen in buttons on alert dialog: -->
|
||||||
|
<item name="android:colorAccent">#212121</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
33
app/src/main/res/xml/debugging_preferences.xml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<PreferenceCategory
|
||||||
|
app:key="logging"
|
||||||
|
app:title="@string/logging_header">
|
||||||
|
|
||||||
|
<ListPreference
|
||||||
|
app:defaultValue="1"
|
||||||
|
app:key="log_level"
|
||||||
|
app:title="@string/log_level_title"
|
||||||
|
app:useSimpleSummaryProvider="true" />
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:key="terminal_view_key_logging_enabled"
|
||||||
|
app:summaryOff="@string/terminal_view_key_logging_off"
|
||||||
|
app:summaryOn="@string/terminal_view_key_logging_on"
|
||||||
|
app:title="@string/terminal_view_key_logging_title" />
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:key="plugin_error_notifications_enabled"
|
||||||
|
app:summaryOff="@string/plugin_error_notifications_off"
|
||||||
|
app:summaryOn="@string/plugin_error_notifications_on"
|
||||||
|
app:title="@string/plugin_error_notifications_title" />
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:key="crash_report_notifications_enabled"
|
||||||
|
app:summaryOff="@string/crash_report_notifications_off"
|
||||||
|
app:summaryOn="@string/crash_report_notifications_on"
|
||||||
|
app:title="@string/crash_report_notifications_title" />
|
||||||
|
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
</PreferenceScreen>
|
||||||
13
app/src/main/res/xml/root_preferences.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
app:title="@string/debugging_preferences"
|
||||||
|
app:summary="Preferences for debugging"
|
||||||
|
app:fragment="com.termux.app.fragments.settings.DebuggingPreferencesFragment"/>
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
app:title="@string/terminal_io_preferences"
|
||||||
|
app:summary="Preferences for terminal I/O"
|
||||||
|
app:fragment="com.termux.app.fragments.settings.TerminalIOPreferencesFragment"/>
|
||||||
|
|
||||||
|
</PreferenceScreen>
|
||||||
@@ -1,30 +1,54 @@
|
|||||||
<shortcuts xmlns:tools="http://schemas.android.com/tools"
|
<shortcuts xmlns:tools="http://schemas.android.com/tools"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
For shortcut.xml:
|
||||||
|
If applicationId in build.gradle is changed from "com.termux", then targetPackage will
|
||||||
|
need to be manually patched since ${applicationId} variable or resource string does not work.
|
||||||
|
If package name in AndroidManifest is changed from "com.termux", then targetClass will
|
||||||
|
need to be manually patched since dot (.) prefix does not work to automatically prefix the
|
||||||
|
package name.
|
||||||
|
-->
|
||||||
|
|
||||||
<shortcut
|
<shortcut
|
||||||
android:shortcutId="new_session"
|
android:shortcutId="new_session"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:icon="@drawable/ic_new_session"
|
android:icon="@drawable/ic_new_session"
|
||||||
android:shortcutShortLabel="@string/new_session"
|
android:shortcutShortLabel="@string/action_new_session"
|
||||||
tools:targetApi="n_mr1">
|
tools:targetApi="n_mr1">
|
||||||
<intent
|
<intent
|
||||||
android:action="android.intent.action.RUN"
|
android:action="android.intent.action.RUN"
|
||||||
android:targetPackage="com.termux"
|
android:targetPackage="com.termux"
|
||||||
android:targetClass="com.termux.app.TermuxActivity"/>
|
android:targetClass="com.termux.app.TermuxActivity"
|
||||||
|
android:name="android.shortcut.conversation"/>
|
||||||
</shortcut>
|
</shortcut>
|
||||||
|
|
||||||
<shortcut
|
<shortcut
|
||||||
android:shortcutId="new_failsafe_session"
|
android:shortcutId="new_failsafe_session"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:icon="@drawable/ic_new_session"
|
android:icon="@drawable/ic_new_session"
|
||||||
android:shortcutShortLabel="@string/new_session_failsafe"
|
android:shortcutShortLabel="@string/action_new_session_failsafe"
|
||||||
tools:targetApi="n_mr1">
|
tools:targetApi="n_mr1">
|
||||||
<intent
|
<intent
|
||||||
android:action="android.intent.action.RUN"
|
android:action="android.intent.action.RUN"
|
||||||
android:targetPackage="com.termux"
|
android:targetPackage="com.termux"
|
||||||
android:targetClass="com.termux.app.TermuxActivity">
|
android:targetClass="com.termux.app.TermuxActivity"
|
||||||
<extra android:name="com.termux.app.failsafe_session" android:value="true" />
|
android:name="android.shortcut.conversation">
|
||||||
|
<extra android:name="com.termux.app.failsafe_session" android:value="true"/>
|
||||||
</intent>
|
</intent>
|
||||||
</shortcut>
|
</shortcut>
|
||||||
|
|
||||||
|
<shortcut
|
||||||
|
android:shortcutId="settings"
|
||||||
|
android:enabled="true"
|
||||||
|
android:icon="@drawable/ic_settings"
|
||||||
|
android:shortcutShortLabel="@string/action_open_settings"
|
||||||
|
tools:targetApi="n_mr1">
|
||||||
|
<intent
|
||||||
|
android:action="android.intent.action.VIEW"
|
||||||
|
android:targetPackage="com.termux"
|
||||||
|
android:targetClass="com.termux.app.activities.SettingsActivity"
|
||||||
|
android:name="android.shortcut.conversation"/>
|
||||||
|
</shortcut>
|
||||||
|
|
||||||
</shortcuts>
|
</shortcuts>
|
||||||
|
|||||||
15
app/src/main/res/xml/terminal_io_preferences.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<PreferenceCategory
|
||||||
|
app:key="keyboard"
|
||||||
|
app:title="@string/keyboard_header">
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:key="soft_keyboard_enabled"
|
||||||
|
app:summaryOff="@string/soft_keyboard_off"
|
||||||
|
app:summaryOn="@string/soft_keyboard_on"
|
||||||
|
app:title="@string/soft_keyboard_title" />
|
||||||
|
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
</PreferenceScreen>
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.termux.app;
|
package com.termux.app;
|
||||||
|
|
||||||
|
import com.termux.shared.data.DataUtils;
|
||||||
|
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
@@ -11,7 +13,7 @@ public class TermuxActivityTest {
|
|||||||
private void assertUrlsAre(String text, String... urls) {
|
private void assertUrlsAre(String text, String... urls) {
|
||||||
LinkedHashSet<String> expected = new LinkedHashSet<>();
|
LinkedHashSet<String> expected = new LinkedHashSet<>();
|
||||||
Collections.addAll(expected, urls);
|
Collections.addAll(expected, urls);
|
||||||
Assert.assertEquals(expected, TermuxActivity.extractUrls(text));
|
Assert.assertEquals(expected, DataUtils.extractUrls(text));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
BIN
art/ic_launcher2.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
art/ic_launcher2_round.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
@@ -4,7 +4,7 @@ buildscript {
|
|||||||
google()
|
google()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:4.0.1'
|
classpath 'com.android.tools.build:gradle:4.1.3'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
13
fastlane/metadata/android/en-US/full_description.txt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
Termux is a terminal emulator application enhanced with a large set of command line utilities ported to Android OS. The main goal is to bring a Linux command line experience to users of mobile devices with no rooting or other special setup required.
|
||||||
|
|
||||||
|
* Enjoy the Bash and Zsh shells.
|
||||||
|
* Edit files with nano and vim.
|
||||||
|
* Access servers over SSH.
|
||||||
|
* Compile C/C++ code with clang.
|
||||||
|
* Use the Python console as a pocket calculator.
|
||||||
|
* Check out projects with Git and Subversion.
|
||||||
|
* Run text-based games with frotz.
|
||||||
|
|
||||||
|
At first start a small base system is being configured. The GNU Bash, Coreutils, Findutils and other core utilities are available out-of-box. Additionally, we provide more than 1000 other packages installable by using the 'pkg' utility which currently is a frontend for the 'apt' package manager. All provided software has been patched and compiled with Android NDK to provide max compatibility with Android OS.
|
||||||
|
|
||||||
|
To learn more about application usage tips and tricks, long-press anywhere on the terminal and select the Help menu option to access Termux Wiki. This resource is also accessible directly in a web browser: https://wiki.termux.com/wiki/Main_Page.
|
||||||
BIN
fastlane/metadata/android/en-US/images/icon.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/4.jpg
Normal file
|
After Width: | Height: | Size: 158 KiB |
1
fastlane/metadata/android/en-US/short_description.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Terminal emulator app with a large set of command line utilities
|
||||||
@@ -15,7 +15,13 @@
|
|||||||
org.gradle.jvmargs=-Xmx2048M
|
org.gradle.jvmargs=-Xmx2048M
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
|
|
||||||
|
termuxVersion=0.111
|
||||||
|
termuxVersionCode=111
|
||||||
|
|
||||||
minSdkVersion=24
|
minSdkVersion=24
|
||||||
targetSdkVersion=28
|
targetSdkVersion=28
|
||||||
ndkVersion=21.3.6528147
|
ndkVersion=22.0.7026061
|
||||||
compileSdkVersion=28
|
compileSdkVersion=29
|
||||||
|
|
||||||
|
markwonVersion=4.6.2
|
||||||
|
|
||||||
|
|||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
// Start https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +1 @@
|
|||||||
include ':app', ':terminal-emulator', ':terminal-view'
|
include ':app', ':termux-shared', ':terminal-emulator', ':terminal-view'
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
plugins {
|
|
||||||
id "com.jfrog.bintray" version "1.7.3"
|
|
||||||
id "com.github.dcendents.android-maven" version "2.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: 'com.android.library'
|
apply plugin: 'com.android.library'
|
||||||
|
apply plugin: 'maven-publish'
|
||||||
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.52'
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
||||||
@@ -52,6 +37,10 @@ android {
|
|||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests.returnDefaultValues = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(Test) {
|
tasks.withType(Test) {
|
||||||
@@ -61,7 +50,34 @@ tasks.withType(Test) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation 'junit:junit:4.13'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: '../scripts/bintray-publish.gradle'
|
task sourceJar(type: Jar) {
|
||||||
|
from android.sourceSets.main.java.srcDirs
|
||||||
|
classifier "sources"
|
||||||
|
}
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
publications {
|
||||||
|
bar(MavenPublication) {
|
||||||
|
groupId 'com.termux'
|
||||||
|
artifactId 'terminal-emulator'
|
||||||
|
version project.properties.termuxVersion
|
||||||
|
artifact(sourceJar)
|
||||||
|
artifact("$buildDir/outputs/aar/terminal-emulator-release.aar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
maven {
|
||||||
|
name = "GitHubPackages"
|
||||||
|
url = uri("https://maven.pkg.github.com/termux/termux-app")
|
||||||
|
|
||||||
|
credentials {
|
||||||
|
username = System.getenv("GH_USERNAME")
|
||||||
|
password = System.getenv("GH_TOKEN")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.termux.terminal;
|
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
public final class EmulatorDebug {
|
|
||||||
|
|
||||||
/** The tag to use with {@link Log}. */
|
|
||||||
public static final String LOG_TAG = "termux";
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -59,6 +59,7 @@ public final class KeyHandler {
|
|||||||
public static final int KEYMOD_ALT = 0x80000000;
|
public static final int KEYMOD_ALT = 0x80000000;
|
||||||
public static final int KEYMOD_CTRL = 0x40000000;
|
public static final int KEYMOD_CTRL = 0x40000000;
|
||||||
public static final int KEYMOD_SHIFT = 0x20000000;
|
public static final int KEYMOD_SHIFT = 0x20000000;
|
||||||
|
public static final int KEYMOD_NUM_LOCK = 0x10000000;
|
||||||
|
|
||||||
private static final Map<String, Integer> TERMCAP_TO_KEYCODE = new HashMap<>();
|
private static final Map<String, Integer> TERMCAP_TO_KEYCODE = new HashMap<>();
|
||||||
|
|
||||||
@@ -145,10 +146,16 @@ public final class KeyHandler {
|
|||||||
keyMod |= KEYMOD_ALT;
|
keyMod |= KEYMOD_ALT;
|
||||||
keyCode &= ~KEYMOD_ALT;
|
keyCode &= ~KEYMOD_ALT;
|
||||||
}
|
}
|
||||||
|
if ((keyCode & KEYMOD_NUM_LOCK) != 0) {
|
||||||
|
keyMod |= KEYMOD_NUM_LOCK;
|
||||||
|
keyCode &= ~KEYMOD_NUM_LOCK;
|
||||||
|
}
|
||||||
return getCode(keyCode, keyMod, cursorKeysApplication, keypadApplication);
|
return getCode(keyCode, keyMod, cursorKeysApplication, keypadApplication);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getCode(int keyCode, int keyMode, boolean cursorApp, boolean keypadApplication) {
|
public static String getCode(int keyCode, int keyMode, boolean cursorApp, boolean keypadApplication) {
|
||||||
|
boolean numLockOn = (keyMode & KEYMOD_NUM_LOCK) != 0;
|
||||||
|
keyMode &= ~KEYMOD_NUM_LOCK;
|
||||||
switch (keyCode) {
|
switch (keyCode) {
|
||||||
case KEYCODE_DPAD_CENTER:
|
case KEYCODE_DPAD_CENTER:
|
||||||
return "\015";
|
return "\015";
|
||||||
@@ -228,8 +235,11 @@ public final class KeyHandler {
|
|||||||
// Just do what xterm and gnome-terminal does:
|
// Just do what xterm and gnome-terminal does:
|
||||||
return prefix + (((keyMode & KEYMOD_CTRL) == 0) ? "\u007F" : "\u0008");
|
return prefix + (((keyMode & KEYMOD_CTRL) == 0) ? "\u007F" : "\u0008");
|
||||||
case KEYCODE_NUM_LOCK:
|
case KEYCODE_NUM_LOCK:
|
||||||
return "\033OP";
|
if (keypadApplication) {
|
||||||
|
return "\033OP";
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case KEYCODE_SPACE:
|
case KEYCODE_SPACE:
|
||||||
// If ctrl is not down, return null so that it goes through normal input processing (which may e.g. cause a
|
// 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):
|
// combining accent to be written):
|
||||||
@@ -249,31 +259,81 @@ public final class KeyHandler {
|
|||||||
case KEYCODE_NUMPAD_COMMA:
|
case KEYCODE_NUMPAD_COMMA:
|
||||||
return ",";
|
return ",";
|
||||||
case KEYCODE_NUMPAD_DOT:
|
case KEYCODE_NUMPAD_DOT:
|
||||||
return keypadApplication ? "\033On" : ".";
|
if (numLockOn) {
|
||||||
|
return keypadApplication ? "\033On" : ".";
|
||||||
|
} else {
|
||||||
|
// DELETE
|
||||||
|
return transformForModifiers("\033[3", keyMode, '~');
|
||||||
|
}
|
||||||
case KEYCODE_NUMPAD_SUBTRACT:
|
case KEYCODE_NUMPAD_SUBTRACT:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'm') : "-";
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'm') : "-";
|
||||||
case KEYCODE_NUMPAD_DIVIDE:
|
case KEYCODE_NUMPAD_DIVIDE:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'o') : "/";
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'o') : "/";
|
||||||
case KEYCODE_NUMPAD_0:
|
case KEYCODE_NUMPAD_0:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'p') : "0";
|
if (numLockOn) {
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'p') : "0";
|
||||||
|
} else {
|
||||||
|
// INSERT
|
||||||
|
return transformForModifiers("\033[2", keyMode, '~');
|
||||||
|
}
|
||||||
case KEYCODE_NUMPAD_1:
|
case KEYCODE_NUMPAD_1:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'q') : "1";
|
if (numLockOn) {
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'q') : "1";
|
||||||
|
} else {
|
||||||
|
// END
|
||||||
|
return (keyMode == 0) ? (cursorApp ? "\033OF" : "\033[F") : transformForModifiers("\033[1", keyMode, 'F');
|
||||||
|
}
|
||||||
case KEYCODE_NUMPAD_2:
|
case KEYCODE_NUMPAD_2:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'r') : "2";
|
if (numLockOn) {
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'r') : "2";
|
||||||
|
} else {
|
||||||
|
// DOWN
|
||||||
|
return (keyMode == 0) ? (cursorApp ? "\033OB" : "\033[B") : transformForModifiers("\033[1", keyMode, 'B');
|
||||||
|
}
|
||||||
case KEYCODE_NUMPAD_3:
|
case KEYCODE_NUMPAD_3:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 's') : "3";
|
if (numLockOn) {
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 's') : "3";
|
||||||
|
} else {
|
||||||
|
// PGDN
|
||||||
|
return "\033[6~";
|
||||||
|
}
|
||||||
case KEYCODE_NUMPAD_4:
|
case KEYCODE_NUMPAD_4:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 't') : "4";
|
if (numLockOn) {
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 't') : "4";
|
||||||
|
} else {
|
||||||
|
// LEFT
|
||||||
|
return (keyMode == 0) ? (cursorApp ? "\033OD" : "\033[D") : transformForModifiers("\033[1", keyMode, 'D');
|
||||||
|
}
|
||||||
case KEYCODE_NUMPAD_5:
|
case KEYCODE_NUMPAD_5:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'u') : "5";
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'u') : "5";
|
||||||
case KEYCODE_NUMPAD_6:
|
case KEYCODE_NUMPAD_6:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'v') : "6";
|
if (numLockOn) {
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'v') : "6";
|
||||||
|
} else {
|
||||||
|
// RIGHT
|
||||||
|
return (keyMode == 0) ? (cursorApp ? "\033OC" : "\033[C") : transformForModifiers("\033[1", keyMode, 'C');
|
||||||
|
}
|
||||||
case KEYCODE_NUMPAD_7:
|
case KEYCODE_NUMPAD_7:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'w') : "7";
|
if (numLockOn) {
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'w') : "7";
|
||||||
|
} else {
|
||||||
|
// HOME
|
||||||
|
return (keyMode == 0) ? (cursorApp ? "\033OH" : "\033[H") : transformForModifiers("\033[1", keyMode, 'H');
|
||||||
|
}
|
||||||
case KEYCODE_NUMPAD_8:
|
case KEYCODE_NUMPAD_8:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'x') : "8";
|
if (numLockOn) {
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'x') : "8";
|
||||||
|
} else {
|
||||||
|
// UP
|
||||||
|
return (keyMode == 0) ? (cursorApp ? "\033OA" : "\033[A") : transformForModifiers("\033[1", keyMode, 'A');
|
||||||
|
}
|
||||||
case KEYCODE_NUMPAD_9:
|
case KEYCODE_NUMPAD_9:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'y') : "9";
|
if (numLockOn) {
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'y') : "9";
|
||||||
|
} else {
|
||||||
|
// PGUP
|
||||||
|
return "\033[5~";
|
||||||
|
}
|
||||||
case KEYCODE_NUMPAD_EQUALS:
|
case KEYCODE_NUMPAD_EQUALS:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'X') : "=";
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'X') : "=";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.termux.terminal;
|
package com.termux.terminal;
|
||||||
|
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@@ -153,6 +152,8 @@ public final class TerminalEmulator {
|
|||||||
/** The terminal session this emulator is bound to. */
|
/** The terminal session this emulator is bound to. */
|
||||||
private final TerminalOutput mSession;
|
private final TerminalOutput mSession;
|
||||||
|
|
||||||
|
TerminalSessionClient mClient;
|
||||||
|
|
||||||
/** Keeps track of the current argument of the current escape sequence. Ranges from 0 to MAX_ESCAPE_PARAMETERS-1. */
|
/** Keeps track of the current argument of the current escape sequence. Ranges from 0 to MAX_ESCAPE_PARAMETERS-1. */
|
||||||
private int mArgIndex;
|
private int mArgIndex;
|
||||||
/** Holds the arguments of the current escape sequence. */
|
/** Holds the arguments of the current escape sequence. */
|
||||||
@@ -227,6 +228,8 @@ public final class TerminalEmulator {
|
|||||||
|
|
||||||
public final TerminalColors mColors = new TerminalColors();
|
public final TerminalColors mColors = new TerminalColors();
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "TerminalEmulator";
|
||||||
|
|
||||||
private boolean isDecsetInternalBitSet(int bit) {
|
private boolean isDecsetInternalBitSet(int bit) {
|
||||||
return (mCurrentDecSetFlags & bit) != 0;
|
return (mCurrentDecSetFlags & bit) != 0;
|
||||||
}
|
}
|
||||||
@@ -279,16 +282,21 @@ public final class TerminalEmulator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public TerminalEmulator(TerminalOutput session, int columns, int rows, int transcriptRows) {
|
public TerminalEmulator(TerminalOutput session, int columns, int rows, int transcriptRows, TerminalSessionClient client) {
|
||||||
mSession = session;
|
mSession = session;
|
||||||
mScreen = mMainBuffer = new TerminalBuffer(columns, transcriptRows, rows);
|
mScreen = mMainBuffer = new TerminalBuffer(columns, transcriptRows, rows);
|
||||||
mAltBuffer = new TerminalBuffer(columns, rows, rows);
|
mAltBuffer = new TerminalBuffer(columns, rows, rows);
|
||||||
|
mClient = client;
|
||||||
mRows = rows;
|
mRows = rows;
|
||||||
mColumns = columns;
|
mColumns = columns;
|
||||||
mTabStop = new boolean[mColumns];
|
mTabStop = new boolean[mColumns];
|
||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void updateTerminalSessionClient(TerminalSessionClient client) {
|
||||||
|
mClient = client;
|
||||||
|
}
|
||||||
|
|
||||||
public TerminalBuffer getScreen() {
|
public TerminalBuffer getScreen() {
|
||||||
return mScreen;
|
return mScreen;
|
||||||
}
|
}
|
||||||
@@ -751,7 +759,7 @@ public final class TerminalEmulator {
|
|||||||
if (internalBit != -1) {
|
if (internalBit != -1) {
|
||||||
value = isDecsetInternalBitSet(internalBit) ? 1 : 2; // 1=set, 2=reset.
|
value = isDecsetInternalBitSet(internalBit) ? 1 : 2; // 1=set, 2=reset.
|
||||||
} else {
|
} else {
|
||||||
Log.e(EmulatorDebug.LOG_TAG, "Got DECRQM for unrecognized private DEC mode=" + mode);
|
mClient.logError(LOG_TAG, "Got DECRQM for unrecognized private DEC mode=" + mode);
|
||||||
value = 0; // 0=not recognized, 3=permanently set, 4=permanently reset
|
value = 0; // 0=not recognized, 3=permanently set, 4=permanently reset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -888,7 +896,7 @@ public final class TerminalEmulator {
|
|||||||
case "&8": // Undo key - ignore.
|
case "&8": // Undo key - ignore.
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
Log.w(EmulatorDebug.LOG_TAG, "Unhandled termcap/terminfo name: '" + trans + "'");
|
mClient.logWarn(LOG_TAG, "Unhandled termcap/terminfo name: '" + trans + "'");
|
||||||
}
|
}
|
||||||
// Respond with invalid request:
|
// Respond with invalid request:
|
||||||
mSession.write("\033P0+r" + part + "\033\\");
|
mSession.write("\033P0+r" + part + "\033\\");
|
||||||
@@ -900,12 +908,12 @@ public final class TerminalEmulator {
|
|||||||
mSession.write("\033P1+r" + part + "=" + hexEncoded + "\033\\");
|
mSession.write("\033P1+r" + part + "=" + hexEncoded + "\033\\");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.e(EmulatorDebug.LOG_TAG, "Invalid device termcap/terminfo name of odd length: " + part);
|
mClient.logError(LOG_TAG, "Invalid device termcap/terminfo name of odd length: " + part);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (LOG_ESCAPE_SEQUENCES)
|
if (LOG_ESCAPE_SEQUENCES)
|
||||||
Log.e(EmulatorDebug.LOG_TAG, "Unrecognized device control string: " + dcs);
|
mClient.logError(LOG_TAG, "Unrecognized device control string: " + dcs);
|
||||||
}
|
}
|
||||||
finishSequence();
|
finishSequence();
|
||||||
}
|
}
|
||||||
@@ -995,7 +1003,7 @@ public final class TerminalEmulator {
|
|||||||
int externalBit = mArgs[i];
|
int externalBit = mArgs[i];
|
||||||
int internalBit = mapDecSetBitToInternalBit(externalBit);
|
int internalBit = mapDecSetBitToInternalBit(externalBit);
|
||||||
if (internalBit == -1) {
|
if (internalBit == -1) {
|
||||||
Log.w(EmulatorDebug.LOG_TAG, "Ignoring request to save/recall decset bit=" + externalBit);
|
mClient.logWarn(LOG_TAG, "Ignoring request to save/recall decset bit=" + externalBit);
|
||||||
} else {
|
} else {
|
||||||
if (b == 's') {
|
if (b == 's') {
|
||||||
mSavedDecSetFlags |= internalBit;
|
mSavedDecSetFlags |= internalBit;
|
||||||
@@ -1182,7 +1190,7 @@ public final class TerminalEmulator {
|
|||||||
// (1) enables this feature for keys except for those with well-known behavior, e.g., Tab, Backarrow and
|
// (1) enables this feature for keys except for those with well-known behavior, e.g., Tab, Backarrow and
|
||||||
// some special control character cases, e.g., Control-Space to make a NUL.
|
// some special control character cases, e.g., Control-Space to make a NUL.
|
||||||
// (2) enables this feature for keys including the exceptions listed.
|
// (2) enables this feature for keys including the exceptions listed.
|
||||||
Log.e(EmulatorDebug.LOG_TAG, "(ignored) CSI > MODIFY RESOURCE: " + getArg0(-1) + " to " + getArg1(-1));
|
mClient.logError(LOG_TAG, "(ignored) CSI > MODIFY RESOURCE: " + getArg0(-1) + " to " + getArg1(-1));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
parseArg(b);
|
parseArg(b);
|
||||||
@@ -1729,7 +1737,7 @@ public final class TerminalEmulator {
|
|||||||
int firstArg = mArgs[i + 1];
|
int firstArg = mArgs[i + 1];
|
||||||
if (firstArg == 2) {
|
if (firstArg == 2) {
|
||||||
if (i + 4 > mArgIndex) {
|
if (i + 4 > mArgIndex) {
|
||||||
Log.w(EmulatorDebug.LOG_TAG, "Too few CSI" + code + ";2 RGB arguments");
|
mClient.logWarn(LOG_TAG, "Too few CSI" + code + ";2 RGB arguments");
|
||||||
} else {
|
} else {
|
||||||
int red = mArgs[i + 2], green = mArgs[i + 3], blue = mArgs[i + 4];
|
int red = mArgs[i + 2], green = mArgs[i + 3], blue = mArgs[i + 4];
|
||||||
if (red < 0 || green < 0 || blue < 0 || red > 255 || green > 255 || blue > 255) {
|
if (red < 0 || green < 0 || blue < 0 || red > 255 || green > 255 || blue > 255) {
|
||||||
@@ -1754,7 +1762,7 @@ public final class TerminalEmulator {
|
|||||||
mBackColor = color;
|
mBackColor = color;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (LOG_ESCAPE_SEQUENCES) Log.w(EmulatorDebug.LOG_TAG, "Invalid color index: " + color);
|
if (LOG_ESCAPE_SEQUENCES) mClient.logWarn(LOG_TAG, "Invalid color index: " + color);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
finishSequenceAndLogError("Invalid ISO-8613-3 SGR first argument: " + firstArg);
|
finishSequenceAndLogError("Invalid ISO-8613-3 SGR first argument: " + firstArg);
|
||||||
@@ -1771,7 +1779,7 @@ public final class TerminalEmulator {
|
|||||||
mBackColor = code - 100 + 8;
|
mBackColor = code - 100 + 8;
|
||||||
} else {
|
} else {
|
||||||
if (LOG_ESCAPE_SEQUENCES)
|
if (LOG_ESCAPE_SEQUENCES)
|
||||||
Log.w(EmulatorDebug.LOG_TAG, String.format("SGR unknown code %d", code));
|
mClient.logWarn(LOG_TAG, String.format("SGR unknown code %d", code));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1905,7 +1913,7 @@ public final class TerminalEmulator {
|
|||||||
String clipboardText = new String(Base64.decode(textParameter.substring(startIndex), 0), StandardCharsets.UTF_8);
|
String clipboardText = new String(Base64.decode(textParameter.substring(startIndex), 0), StandardCharsets.UTF_8);
|
||||||
mSession.clipboardText(clipboardText);
|
mSession.clipboardText(clipboardText);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(EmulatorDebug.LOG_TAG, "OSC Manipulate selection, invalid string '" + textParameter + "");
|
mClient.logError(LOG_TAG, "OSC Manipulate selection, invalid string '" + textParameter + "");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 104:
|
case 104:
|
||||||
@@ -2101,7 +2109,7 @@ public final class TerminalEmulator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void finishSequenceAndLogError(String error) {
|
private void finishSequenceAndLogError(String error) {
|
||||||
if (LOG_ESCAPE_SEQUENCES) Log.w(EmulatorDebug.LOG_TAG, error);
|
if (LOG_ESCAPE_SEQUENCES) mClient.logWarn(LOG_TAG, error);
|
||||||
finishSequence();
|
finishSequence();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ public abstract class TerminalOutput {
|
|||||||
|
|
||||||
/** Write a string using the UTF-8 encoding to the terminal client. */
|
/** Write a string using the UTF-8 encoding to the terminal client. */
|
||||||
public final void write(String data) {
|
public final void write(String data) {
|
||||||
|
if (data == null) return;
|
||||||
byte[] bytes = data.getBytes(StandardCharsets.UTF_8);
|
byte[] bytes = data.getBytes(StandardCharsets.UTF_8);
|
||||||
write(bytes, 0, bytes.length);
|
write(bytes, 0, bytes.length);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import android.os.Message;
|
|||||||
import android.system.ErrnoException;
|
import android.system.ErrnoException;
|
||||||
import android.system.Os;
|
import android.system.Os;
|
||||||
import android.system.OsConstants;
|
import android.system.OsConstants;
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileDescriptor;
|
import java.io.FileDescriptor;
|
||||||
@@ -31,41 +30,6 @@ import java.util.UUID;
|
|||||||
*/
|
*/
|
||||||
public final class TerminalSession extends TerminalOutput {
|
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);
|
|
||||||
|
|
||||||
void onColorsChanged(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_NEW_INPUT = 1;
|
||||||
private static final int MSG_PROCESS_EXITED = 4;
|
private static final int MSG_PROCESS_EXITED = 4;
|
||||||
|
|
||||||
@@ -87,7 +51,7 @@ public final class TerminalSession extends TerminalOutput {
|
|||||||
private final byte[] mUtf8InputBuffer = new byte[5];
|
private final byte[] mUtf8InputBuffer = new byte[5];
|
||||||
|
|
||||||
/** Callback which gets notified when a session finishes or changes title. */
|
/** Callback which gets notified when a session finishes or changes title. */
|
||||||
final SessionChangedCallback mChangeCallback;
|
TerminalSessionClient mClient;
|
||||||
|
|
||||||
/** The pid of the shell process. 0 if not started and -1 if finished running. */
|
/** The pid of the shell process. 0 if not started and -1 if finished running. */
|
||||||
int mShellPid;
|
int mShellPid;
|
||||||
@@ -104,52 +68,32 @@ public final class TerminalSession extends TerminalOutput {
|
|||||||
/** Set by the application for user identification of session, not by terminal. */
|
/** Set by the application for user identification of session, not by terminal. */
|
||||||
public String mSessionName;
|
public String mSessionName;
|
||||||
|
|
||||||
@SuppressLint("HandlerLeak")
|
final Handler mMainThreadHandler = new MainThreadHandler();
|
||||||
final Handler mMainThreadHandler = new Handler() {
|
|
||||||
final byte[] mReceiveBuffer = new byte[4 * 1024];
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleMessage(Message msg) {
|
|
||||||
int bytesRead = mProcessToTerminalIOQueue.read(mReceiveBuffer, false);
|
|
||||||
if (bytesRead > 0) {
|
|
||||||
mEmulator.append(mReceiveBuffer, bytesRead);
|
|
||||||
notifyScreenUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 += " (code " + exitCode + ")";
|
|
||||||
} else if (exitCode < 0) {
|
|
||||||
// Negated signal.
|
|
||||||
exitDescription += " (signal " + (-exitCode) + ")";
|
|
||||||
}
|
|
||||||
exitDescription += " - press Enter]";
|
|
||||||
|
|
||||||
byte[] bytesToWrite = exitDescription.getBytes(StandardCharsets.UTF_8);
|
|
||||||
mEmulator.append(bytesToWrite, bytesToWrite.length);
|
|
||||||
notifyScreenUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private final String mShellPath;
|
private final String mShellPath;
|
||||||
private final String mCwd;
|
private final String mCwd;
|
||||||
private final String[] mArgs;
|
private final String[] mArgs;
|
||||||
private final String[] mEnv;
|
private final String[] mEnv;
|
||||||
|
|
||||||
public TerminalSession(String shellPath, String cwd, String[] args, String[] env, SessionChangedCallback changeCallback) {
|
private static final String LOG_TAG = "TerminalSession";
|
||||||
mChangeCallback = changeCallback;
|
|
||||||
|
|
||||||
|
public TerminalSession(String shellPath, String cwd, String[] args, String[] env, TerminalSessionClient client) {
|
||||||
this.mShellPath = shellPath;
|
this.mShellPath = shellPath;
|
||||||
this.mCwd = cwd;
|
this.mCwd = cwd;
|
||||||
this.mArgs = args;
|
this.mArgs = args;
|
||||||
this.mEnv = env;
|
this.mEnv = env;
|
||||||
|
this.mClient = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param client The {@link TerminalSessionClient} interface implementation to allow
|
||||||
|
* for communication between {@link TerminalSession} and its client.
|
||||||
|
*/
|
||||||
|
public void updateTerminalSessionClient(TerminalSessionClient client) {
|
||||||
|
mClient = client;
|
||||||
|
|
||||||
|
if (mEmulator != null)
|
||||||
|
mEmulator.updateTerminalSessionClient(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Inform the attached pty of the new size and reflow or initialize the emulator. */
|
/** Inform the attached pty of the new size and reflow or initialize the emulator. */
|
||||||
@@ -174,13 +118,13 @@ public final class TerminalSession extends TerminalOutput {
|
|||||||
* @param rows The number of rows in the terminal window.
|
* @param rows The number of rows in the terminal window.
|
||||||
*/
|
*/
|
||||||
public void initializeEmulator(int columns, int rows) {
|
public void initializeEmulator(int columns, int rows) {
|
||||||
mEmulator = new TerminalEmulator(this, columns, rows, /* transcript= */2000);
|
mEmulator = new TerminalEmulator(this, columns, rows, /* transcript= */2000, mClient);
|
||||||
|
|
||||||
int[] processId = new int[1];
|
int[] processId = new int[1];
|
||||||
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);
|
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);
|
||||||
mShellPid = processId[0];
|
mShellPid = processId[0];
|
||||||
|
|
||||||
final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor);
|
final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor, mClient);
|
||||||
|
|
||||||
new Thread("TermSessionInputReader[pid=" + mShellPid + "]") {
|
new Thread("TermSessionInputReader[pid=" + mShellPid + "]") {
|
||||||
@Override
|
@Override
|
||||||
@@ -246,23 +190,23 @@ public final class TerminalSession extends TerminalOutput {
|
|||||||
} else if (codePoint <= /* 11 bits */0b11111111111) {
|
} else if (codePoint <= /* 11 bits */0b11111111111) {
|
||||||
/* 110xxxxx leading byte with leading 5 bits */
|
/* 110xxxxx leading byte with leading 5 bits */
|
||||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11000000 | (codePoint >> 6));
|
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11000000 | (codePoint >> 6));
|
||||||
/* 10xxxxxx continuation byte with following 6 bits */
|
/* 10xxxxxx continuation byte with following 6 bits */
|
||||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
|
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
|
||||||
} else if (codePoint <= /* 16 bits */0b1111111111111111) {
|
} else if (codePoint <= /* 16 bits */0b1111111111111111) {
|
||||||
/* 1110xxxx leading byte with leading 4 bits */
|
/* 1110xxxx leading byte with leading 4 bits */
|
||||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11100000 | (codePoint >> 12));
|
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11100000 | (codePoint >> 12));
|
||||||
/* 10xxxxxx continuation byte with following 6 bits */
|
/* 10xxxxxx continuation byte with following 6 bits */
|
||||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 6) & 0b111111));
|
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 6) & 0b111111));
|
||||||
/* 10xxxxxx continuation byte with following 6 bits */
|
/* 10xxxxxx continuation byte with following 6 bits */
|
||||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
|
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
|
||||||
} else { /* We have checked codePoint <= 1114111 above, so we have max 21 bits = 0b111111111111111111111 */
|
} else { /* We have checked codePoint <= 1114111 above, so we have max 21 bits = 0b111111111111111111111 */
|
||||||
/* 11110xxx leading byte with leading 3 bits */
|
/* 11110xxx leading byte with leading 3 bits */
|
||||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11110000 | (codePoint >> 18));
|
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11110000 | (codePoint >> 18));
|
||||||
/* 10xxxxxx continuation byte with following 6 bits */
|
/* 10xxxxxx continuation byte with following 6 bits */
|
||||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 12) & 0b111111));
|
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 12) & 0b111111));
|
||||||
/* 10xxxxxx continuation byte with following 6 bits */
|
/* 10xxxxxx continuation byte with following 6 bits */
|
||||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 6) & 0b111111));
|
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 6) & 0b111111));
|
||||||
/* 10xxxxxx continuation byte with following 6 bits */
|
/* 10xxxxxx continuation byte with following 6 bits */
|
||||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
|
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
|
||||||
}
|
}
|
||||||
write(mUtf8InputBuffer, 0, bufferPosition);
|
write(mUtf8InputBuffer, 0, bufferPosition);
|
||||||
@@ -272,9 +216,9 @@ public final class TerminalSession extends TerminalOutput {
|
|||||||
return mEmulator;
|
return mEmulator;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Notify the {@link #mChangeCallback} that the screen has changed. */
|
/** Notify the {@link #mClient} that the screen has changed. */
|
||||||
protected void notifyScreenUpdate() {
|
protected void notifyScreenUpdate() {
|
||||||
mChangeCallback.onTextChanged(this);
|
mClient.onTextChanged(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Reset state for terminal emulator state. */
|
/** Reset state for terminal emulator state. */
|
||||||
@@ -289,7 +233,7 @@ public final class TerminalSession extends TerminalOutput {
|
|||||||
try {
|
try {
|
||||||
Os.kill(mShellPid, OsConstants.SIGKILL);
|
Os.kill(mShellPid, OsConstants.SIGKILL);
|
||||||
} catch (ErrnoException e) {
|
} catch (ErrnoException e) {
|
||||||
Log.w("termux", "Failed sending SIGKILL: " + e.getMessage());
|
mClient.logWarn(LOG_TAG, "Failed sending SIGKILL: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -309,7 +253,7 @@ public final class TerminalSession extends TerminalOutput {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void titleChanged(String oldTitle, String newTitle) {
|
public void titleChanged(String oldTitle, String newTitle) {
|
||||||
mChangeCallback.onTitleChanged(this);
|
mClient.onTitleChanged(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized boolean isRunning() {
|
public synchronized boolean isRunning() {
|
||||||
@@ -323,17 +267,17 @@ public final class TerminalSession extends TerminalOutput {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void clipboardText(String text) {
|
public void clipboardText(String text) {
|
||||||
mChangeCallback.onClipboardText(this, text);
|
mClient.onClipboardText(this, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBell() {
|
public void onBell() {
|
||||||
mChangeCallback.onBell(this);
|
mClient.onBell(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onColorsChanged() {
|
public void onColorsChanged() {
|
||||||
mChangeCallback.onColorsChanged(this);
|
mClient.onColorsChanged(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getPid() {
|
public int getPid() {
|
||||||
@@ -356,10 +300,65 @@ public final class TerminalSession extends TerminalOutput {
|
|||||||
return outputPath;
|
return outputPath;
|
||||||
}
|
}
|
||||||
} catch (IOException | SecurityException e) {
|
} catch (IOException | SecurityException e) {
|
||||||
Log.e(EmulatorDebug.LOG_TAG, "Error getting current directory", e);
|
mClient.logStackTraceWithMessage(LOG_TAG, "Error getting current directory", e);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static FileDescriptor wrapFileDescriptor(int fileDescriptor, TerminalSessionClient client) {
|
||||||
|
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) {
|
||||||
|
client.logStackTraceWithMessage(LOG_TAG, "Error accessing FileDescriptor#descriptor private field", e);
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("HandlerLeak")
|
||||||
|
class MainThreadHandler extends Handler {
|
||||||
|
|
||||||
|
final byte[] mReceiveBuffer = new byte[4 * 1024];
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleMessage(Message msg) {
|
||||||
|
int bytesRead = mProcessToTerminalIOQueue.read(mReceiveBuffer, false);
|
||||||
|
if (bytesRead > 0) {
|
||||||
|
mEmulator.append(mReceiveBuffer, bytesRead);
|
||||||
|
notifyScreenUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.what == MSG_PROCESS_EXITED) {
|
||||||
|
int exitCode = (Integer) msg.obj;
|
||||||
|
cleanupResources(exitCode);
|
||||||
|
|
||||||
|
String exitDescription = "\r\n[Process completed";
|
||||||
|
if (exitCode > 0) {
|
||||||
|
// Non-zero process exit.
|
||||||
|
exitDescription += " (code " + exitCode + ")";
|
||||||
|
} else if (exitCode < 0) {
|
||||||
|
// Negated signal.
|
||||||
|
exitDescription += " (signal " + (-exitCode) + ")";
|
||||||
|
}
|
||||||
|
exitDescription += " - press Enter]";
|
||||||
|
|
||||||
|
byte[] bytesToWrite = exitDescription.getBytes(StandardCharsets.UTF_8);
|
||||||
|
mEmulator.append(bytesToWrite, bytesToWrite.length);
|
||||||
|
notifyScreenUpdate();
|
||||||
|
|
||||||
|
mClient.onSessionFinished(TerminalSession.this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.termux.terminal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The interface for communication between {@link TerminalSession} and its client. It is used to
|
||||||
|
* send callbacks to the client when {@link TerminalSession} changes or for sending other
|
||||||
|
* back data to the client like logs.
|
||||||
|
*/
|
||||||
|
public interface TerminalSessionClient {
|
||||||
|
|
||||||
|
void onTextChanged(TerminalSession changedSession);
|
||||||
|
|
||||||
|
void onTitleChanged(TerminalSession changedSession);
|
||||||
|
|
||||||
|
void onSessionFinished(TerminalSession finishedSession);
|
||||||
|
|
||||||
|
void onClipboardText(TerminalSession session, String text);
|
||||||
|
|
||||||
|
void onBell(TerminalSession session);
|
||||||
|
|
||||||
|
void onColorsChanged(TerminalSession session);
|
||||||
|
|
||||||
|
|
||||||
|
void logError(String tag, String message);
|
||||||
|
|
||||||
|
void logWarn(String tag, String message);
|
||||||
|
|
||||||
|
void logInfo(String tag, String message);
|
||||||
|
|
||||||
|
void logDebug(String tag, String message);
|
||||||
|
|
||||||
|
void logVerbose(String tag, String message);
|
||||||
|
|
||||||
|
void logStackTraceWithMessage(String tag, String message, Exception e);
|
||||||
|
|
||||||
|
void logStackTrace(String tag, Exception e);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -88,7 +88,7 @@ static int create_subprocess(JNIEnv* env,
|
|||||||
struct dirent* entry;
|
struct dirent* entry;
|
||||||
while ((entry = readdir(self_dir)) != NULL) {
|
while ((entry = readdir(self_dir)) != NULL) {
|
||||||
int fd = atoi(entry->d_name);
|
int fd = atoi(entry->d_name);
|
||||||
if(fd > 2 && fd != self_dir_fd) close(fd);
|
if (fd > 2 && fd != self_dir_fd) close(fd);
|
||||||
}
|
}
|
||||||
closedir(self_dir);
|
closedir(self_dir);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ public class ByteQueueTest extends TestCase {
|
|||||||
|
|
||||||
public void testCompleteWrites() throws Exception {
|
public void testCompleteWrites() throws Exception {
|
||||||
ByteQueue q = new ByteQueue(10);
|
ByteQueue q = new ByteQueue(10);
|
||||||
assertTrue(q.write(new byte[]{1, 2, 3}, 0, 3));
|
assertTrue(q.write(new byte[]{1, 2, 3}, 0, 3));
|
||||||
|
|
||||||
byte[] arr = new byte[10];
|
byte[] arr = new byte[10];
|
||||||
assertEquals(3, q.read(arr, true));
|
assertEquals(3, q.read(arr, true));
|
||||||
assertArrayEquals(new byte[]{1, 2, 3}, new byte[]{arr[0], arr[1], arr[2]});
|
assertArrayEquals(new byte[]{1, 2, 3}, new byte[]{arr[0], arr[1], arr[2]});
|
||||||
|
|
||||||
assertTrue(q.write(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 0, 10));
|
assertTrue(q.write(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 0, 10));
|
||||||
assertEquals(10, q.read(arr, true));
|
assertEquals(10, q.read(arr, true));
|
||||||
assertArrayEquals(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, arr);
|
assertArrayEquals(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, arr);
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ public class ByteQueueTest extends TestCase {
|
|||||||
public void testWriteNotesClosing() throws Exception {
|
public void testWriteNotesClosing() throws Exception {
|
||||||
ByteQueue q = new ByteQueue(10);
|
ByteQueue q = new ByteQueue(10);
|
||||||
q.close();
|
q.close();
|
||||||
assertFalse(q.write(new byte[]{1, 2, 3}, 0, 3));
|
assertFalse(q.write(new byte[]{1, 2, 3}, 0, 3));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testReadNonBlocking() throws Exception {
|
public void testReadNonBlocking() throws Exception {
|
||||||
|
|||||||