Compare commits
306 Commits
github-rel
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
53f75a8da3 | |
|
|
bcb61c387c | |
|
|
c6ec1cb64f | |
|
|
382c9708b3 | |
|
|
edd44d2ce9 | |
|
|
4183ffa122 | |
|
|
8aca6dbbf4 | |
|
|
1812a405ee | |
|
|
338fb66e5c | |
|
|
f852dca0ef | |
|
|
8174a995ae | |
|
|
7bceab88e2 | |
|
|
2c1bd650b0 | |
|
|
4a9ad910dd | |
|
|
9d2ac14557 | |
|
|
c6e2ca7fde | |
|
|
da3a0ac4e2 | |
|
|
4de0caac5b | |
|
|
bc321d0a7c | |
|
|
9ee1c9d5ad | |
|
|
a988383e01 | |
|
|
d2cd6ac2e5 | |
|
|
52da00e5c2 | |
|
|
b0e1dbc3da | |
|
|
078eea2b74 | |
|
|
fb01127ff2 | |
|
|
b84dc703e8 | |
|
|
8be53336c3 | |
|
|
d90e3fbca1 | |
|
|
245158ceb9 | |
|
|
6c00f1fc61 | |
|
|
03142590ff | |
|
|
4443b657bf | |
|
|
a8a69c6d80 | |
|
|
438cd73fff | |
|
|
36d811ea7d | |
|
|
c2d57f2ed8 | |
|
|
661c37501f | |
|
|
f80b46487d | |
|
|
a5de3d3a2b | |
|
|
57e4ef456b | |
|
|
d90be9cd50 | |
|
|
062c9771a9 | |
|
|
2f40df91e5 | |
|
|
755b752a95 | |
|
|
67f4891580 | |
|
|
7b19cd2f5a | |
|
|
882da34fcd | |
|
|
8e3a8980a8 | |
|
|
e4385832b7 | |
|
|
3b5018b4c7 | |
|
|
c84d4804c8 | |
|
|
6727bbecc4 | |
|
|
e27f9fa979 | |
|
|
e2f0edf4d2 | |
|
|
c5b69975e1 | |
|
|
cce78cc274 | |
|
|
32cd8a9384 | |
|
|
eef5ac43a7 | |
|
|
55cdef01e7 | |
|
|
7c262b8d99 | |
|
|
06230f95df | |
|
|
9d308c2331 | |
|
|
11d8e4ff8f | |
|
|
66a9495d91 | |
|
|
33295decbb | |
|
|
ba1fb850bf | |
|
|
1240c5ca47 | |
|
|
c1dca29076 | |
|
|
93eafffb90 | |
|
|
9b274f9a0d | |
|
|
b800f1cc81 | |
|
|
2ac7fd1e56 | |
|
|
c6dce12510 | |
|
|
2f5a6f7de6 | |
|
|
82f83a2970 | |
|
|
b1c043d540 | |
|
|
cff6cff609 | |
|
|
29cf9820e1 | |
|
|
c8a74dc588 | |
|
|
20dee0e940 | |
|
|
3516f1979f | |
|
|
5bc3d2db8d | |
|
|
3f7a939313 | |
|
|
0c14c291b2 | |
|
|
63d035ce39 | |
|
|
8c1749ef96 | |
|
|
6c56073958 | |
|
|
061dc776bd | |
|
|
211340781b | |
|
|
605dd6c192 | |
|
|
4646aca597 | |
|
|
f1d411a5ab | |
|
|
5fc2b4cd4a | |
|
|
a2df7d791a | |
|
|
82b1580312 | |
|
|
e92a6db06b | |
|
|
4c47f4f732 | |
|
|
26ff978b0f | |
|
|
b80126fd61 | |
|
|
162469f7ce | |
|
|
e75680a884 | |
|
|
af6ac30bb1 | |
|
|
79d799a99d | |
|
|
841c41bf37 | |
|
|
c2ddc23ae5 | |
|
|
b69630355a | |
|
|
42eee49d30 | |
|
|
03e1d14e1e | |
|
|
f76c20d036 | |
|
|
150b1ff99c | |
|
|
ebdab0e59c | |
|
|
afc06cfd0a | |
|
|
9749360caa | |
|
|
29d05cc72c | |
|
|
2998558e9f | |
|
|
13d93ccac7 | |
|
|
f102ea20b2 | |
|
|
0328d15ea7 | |
|
|
f9e9193c4e | |
|
|
790481b802 | |
|
|
1788013c80 | |
|
|
5759411109 | |
|
|
0fd354a469 | |
|
|
042487c2b4 | |
|
|
b96fcb78fd | |
|
|
9547869a52 | |
|
|
d29e20b0d0 | |
|
|
0c22067b5e | |
|
|
d287734aba | |
|
|
46cfea09ec | |
|
|
980bf8f0ae | |
|
|
231ecff5f0 | |
|
|
c1c46dfcfc | |
|
|
37f08c4fcc | |
|
|
a50387b553 | |
|
|
30cb848639 | |
|
|
b04f209f17 | |
|
|
7b222ba392 | |
|
|
899ef71e17 | |
|
|
4d084c02e7 | |
|
|
18a1a33e83 | |
|
|
7677633e8f | |
|
|
007bef8132 | |
|
|
5290ce1f77 | |
|
|
ab9b620c88 | |
|
|
4e08f76fd2 | |
|
|
c549988434 | |
|
|
55dcd09a09 | |
|
|
677a580042 | |
|
|
fa829623a8 | |
|
|
14e9a8b6fc | |
|
|
a1719d91b3 | |
|
|
9143ebdc22 | |
|
|
623aaebb4a | |
|
|
6213b7f782 | |
|
|
0b4f456132 | |
|
|
b950efec27 | |
|
|
4b3b1a5b6a | |
|
|
7f7d889dd0 | |
|
|
53f26c9659 | |
|
|
2aa7f43d1c | |
|
|
5f8a922201 | |
|
|
9a71074c7d | |
|
|
69cc65c3ac | |
|
|
bcd8f4c419 | |
|
|
6bda7c4fc4 | |
|
|
89a08ff01a | |
|
|
c81d9c3346 | |
|
|
58c3d427e8 | |
|
|
1b9ca91da5 | |
|
|
9c7ec0cebd | |
|
|
6b60adc079 | |
|
|
5116d886c3 | |
|
|
02ab8324e9 | |
|
|
c095a6184b | |
|
|
cc981d8a03 | |
|
|
007f9cd7f1 | |
|
|
b025872029 | |
|
|
2851175d8b | |
|
|
3dee2eb486 | |
|
|
33b88b5d4b | |
|
|
f366db0cb3 | |
|
|
4aca16326c | |
|
|
e597ece75f | |
|
|
9e06bfce1f | |
|
|
5794ab9a56 | |
|
|
ee32ef0c7e | |
|
|
8746db0d22 | |
|
|
ce12b8ad2d | |
|
|
87a79a9b24 | |
|
|
caa13b7047 | |
|
|
5e820ad249 | |
|
|
25d21e9d2e | |
|
|
dd378738e3 | |
|
|
93d57f053b | |
|
|
26e0fa2b9e | |
|
|
d25f7afd97 | |
|
|
e0074f280f | |
|
|
cc58ddde31 | |
|
|
477b36acd1 | |
|
|
5f00531381 | |
|
|
4dbfc1fac8 | |
|
|
4b07e4f4c0 | |
|
|
621545dd0a | |
|
|
9a65aa4589 | |
|
|
021cb60e23 | |
|
|
14c5fc7b1e | |
|
|
792c33c9a5 | |
|
|
760ae78aff | |
|
|
c3ac30e2fb | |
|
|
b94dc7eea9 | |
|
|
05283bd774 | |
|
|
6d944b5f7f | |
|
|
bd004514df | |
|
|
270e41fae5 | |
|
|
68cdbd6ff4 | |
|
|
280e284488 | |
|
|
a01ff018b3 | |
|
|
f8e7ada143 | |
|
|
f33758c7c0 | |
|
|
c567cc3b92 | |
|
|
43858dfbb1 | |
|
|
b8c3db0b6e | |
|
|
622ff4fad4 | |
|
|
a56ed5771d | |
|
|
2a1c5a70da | |
|
|
9b5aad9416 | |
|
|
95d7a154a4 | |
|
|
172a75e578 | |
|
|
4fd48a5aed | |
|
|
81dd113157 | |
|
|
2452399a13 | |
|
|
6631599fb6 | |
|
|
f3f434af92 | |
|
|
6fea1fbddc | |
|
|
a3cd058fb4 | |
|
|
b435d94888 | |
|
|
3898ebdc74 | |
|
|
1f3d3616a4 | |
|
|
b45ff8a407 | |
|
|
bf10c72661 | |
|
|
1fb4fe2510 | |
|
|
8e7e355fcb | |
|
|
0fa0738cf6 | |
|
|
998499d991 | |
|
|
20a70b1a22 | |
|
|
5d202d082f | |
|
|
bb1584decb | |
|
|
c1a0d6deff | |
|
|
3f84b5345f | |
|
|
006bfeac8d | |
|
|
d222102635 | |
|
|
7a386a7f2a | |
|
|
b79ed509f1 | |
|
|
1b794b3518 | |
|
|
0a3efc537d | |
|
|
36e49707ec | |
|
|
f857bf2968 | |
|
|
b69d14119e | |
|
|
8c43b7f0a1 | |
|
|
6ff5572999 | |
|
|
8e506859a6 | |
|
|
361bfb3961 | |
|
|
549a772d45 | |
|
|
37b9bcf5af | |
|
|
7bbc12c7c9 | |
|
|
74b23cb209 | |
|
|
b559d5a0bd | |
|
|
3e518a6a75 | |
|
|
d96883c4d6 | |
|
|
28ecb64992 | |
|
|
5d64f1225c | |
|
|
aed4b96a31 | |
|
|
5b2aca9cf7 | |
|
|
93d738ae63 | |
|
|
f7ebcae7b3 | |
|
|
63c106c746 | |
|
|
2c0e9c6c5c | |
|
|
9eeb2babd7 | |
|
|
32dd7eab03 | |
|
|
50a97b1977 | |
|
|
f4a997b7dd | |
|
|
3c202928b4 | |
|
|
aca0000ee6 | |
|
|
5252fbbe11 | |
|
|
304aed3063 | |
|
|
cbac7c8fbd | |
|
|
65252dc640 | |
|
|
2c6d009657 | |
|
|
a987246bd8 | |
|
|
059feaacf1 | |
|
|
f62997a60e | |
|
|
79980a07a8 | |
|
|
4faf2b9d28 | |
|
|
701b5ccd5c | |
|
|
9798b30c76 | |
|
|
fa91205bca | |
|
|
64adc521de | |
|
|
9814438ae5 | |
|
|
ae7f141aca | |
|
|
fd4159f1ba | |
|
|
1327cef7b4 | |
|
|
0da1984b59 | |
|
|
09412da9d7 | |
|
|
009c128052 |
|
|
@ -1,2 +1 @@
|
|||
patreon: termux
|
||||
custom: https://paypal.me/fornwall
|
||||
custom: https://termux.dev/donate
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
commit-message:
|
||||
# Prefix all commit messages with "Changed: "
|
||||
prefix: "Changed"
|
||||
|
|
@ -8,16 +8,23 @@ on:
|
|||
jobs:
|
||||
attach-apks:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
package_variant: [ apt-android-7, apt-android-5 ]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ env.GITHUB_REF }}
|
||||
|
||||
- name: Build and attach APKs to release
|
||||
shell: bash {0}
|
||||
env:
|
||||
PACKAGE_VARIANT: ${{ matrix.package_variant }}
|
||||
run: |
|
||||
exit_on_error() {
|
||||
echo "$1"
|
||||
|
|
@ -34,13 +41,14 @@ jobs:
|
|||
fi
|
||||
|
||||
APK_DIR_PATH="./app/build/outputs/apk/debug"
|
||||
APK_VERSION_TAG="$RELEASE_VERSION_NAME+github-debug"
|
||||
APK_VERSION_TAG="$RELEASE_VERSION_NAME+${{ env.PACKAGE_VARIANT }}-github-debug"
|
||||
APK_BASENAME_PREFIX="termux-app_$APK_VERSION_TAG"
|
||||
|
||||
echo "Building APKs for '$RELEASE_VERSION_NAME' release"
|
||||
echo "Building APKs for 'APK_VERSION_TAG' release"
|
||||
export TERMUX_APK_VERSION_TAG="$APK_VERSION_TAG" # Used by app/build.gradle
|
||||
export TERMUX_PACKAGE_VARIANT="${{ env.PACKAGE_VARIANT }}" # Used by app/build.gradle
|
||||
if ! ./gradlew assembleDebug; then
|
||||
exit_on_error "Build failed for '$RELEASE_VERSION_NAME' release."
|
||||
exit_on_error "Build failed for '$APK_VERSION_TAG' release."
|
||||
fi
|
||||
|
||||
echo "Validating APKs"
|
||||
|
|
@ -58,8 +66,8 @@ jobs:
|
|||
"${APK_BASENAME_PREFIX}_armeabi-v7a.apk" \
|
||||
"${APK_BASENAME_PREFIX}_x86_64.apk" \
|
||||
"${APK_BASENAME_PREFIX}_x86.apk" \
|
||||
> sha256sums); then
|
||||
exit_on_error "Generate sha25sums failed for '$RELEASE_VERSION_NAME' release."
|
||||
> "${APK_BASENAME_PREFIX}_sha256sums"); then
|
||||
exit_on_error "Generate sha25sums failed for '$APK_VERSION_TAG' release."
|
||||
fi
|
||||
|
||||
echo "Attaching APKs to github release"
|
||||
|
|
@ -70,7 +78,7 @@ jobs:
|
|||
-a "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_armeabi-v7a.apk" \
|
||||
-a "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_x86_64.apk" \
|
||||
-a "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_x86.apk" \
|
||||
-a "$APK_DIR_PATH/sha256sums" \
|
||||
-a "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_sha256sums" \
|
||||
"$RELEASE_VERSION_NAME"; then
|
||||
exit_on_error "Attach APKs to release failed for '$RELEASE_VERSION_NAME' release."
|
||||
exit_on_error "Attach APKs to release failed for '$APK_VERSION_TAG' release."
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- master
|
||||
- 'github-releases/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
|
@ -11,16 +12,34 @@ on:
|
|||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
package_variant: [ apt-android-7, apt-android-5 ]
|
||||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup java 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Build APKs
|
||||
shell: bash {0}
|
||||
env:
|
||||
PACKAGE_VARIANT: ${{ matrix.package_variant }}
|
||||
run: |
|
||||
exit_on_error() { echo "$1"; exit 1; }
|
||||
|
||||
echo "Setting vars"
|
||||
|
||||
if [ "$GITHUB_EVENT_NAME" == "pull_request" ]; then
|
||||
GITHUB_SHA="${{ github.event.pull_request.head.sha }}" # Do not use last merge commit set in GITHUB_SHA
|
||||
fi
|
||||
|
||||
# Set RELEASE_VERSION_NAME to "<CURRENT_VERSION_NAME>+<last_commit_hash>"
|
||||
CURRENT_VERSION_NAME_REGEX='\s+versionName "([^"]+)"$'
|
||||
CURRENT_VERSION_NAME="$(grep -m 1 -E "$CURRENT_VERSION_NAME_REGEX" ./app/build.gradle | sed -r "s/$CURRENT_VERSION_NAME_REGEX/\1/")"
|
||||
|
|
@ -30,7 +49,7 @@ jobs:
|
|||
fi
|
||||
|
||||
APK_DIR_PATH="./app/build/outputs/apk/debug"
|
||||
APK_VERSION_TAG="$RELEASE_VERSION_NAME-github-debug" # Note the "-", GITHUB_SHA will already have "+" before it
|
||||
APK_VERSION_TAG="$RELEASE_VERSION_NAME-${{ env.PACKAGE_VARIANT }}-github-debug" # Note the "-", GITHUB_SHA will already have "+" before it
|
||||
APK_BASENAME_PREFIX="termux-app_$APK_VERSION_TAG"
|
||||
|
||||
# Used by attachment steps later
|
||||
|
|
@ -38,11 +57,12 @@ jobs:
|
|||
echo "APK_VERSION_TAG=$APK_VERSION_TAG" >> $GITHUB_ENV
|
||||
echo "APK_BASENAME_PREFIX=$APK_BASENAME_PREFIX" >> $GITHUB_ENV
|
||||
|
||||
echo "Building APKs for '$RELEASE_VERSION_NAME' build"
|
||||
echo "Building APKs for 'APK_VERSION_TAG' build"
|
||||
export TERMUX_APP_VERSION_NAME="${RELEASE_VERSION_NAME/v/}" # Used by app/build.gradle
|
||||
export TERMUX_APK_VERSION_TAG="$APK_VERSION_TAG" # Used by app/build.gradle
|
||||
export TERMUX_PACKAGE_VARIANT="${{ env.PACKAGE_VARIANT }}" # Used by app/build.gradle
|
||||
if ! ./gradlew assembleDebug; then
|
||||
exit_on_error "Build failed for '$RELEASE_VERSION_NAME' build."
|
||||
exit_on_error "Build failed for '$APK_VERSION_TAG' build."
|
||||
fi
|
||||
|
||||
echo "Validating APKs"
|
||||
|
|
@ -60,12 +80,12 @@ jobs:
|
|||
"${APK_BASENAME_PREFIX}_armeabi-v7a.apk" \
|
||||
"${APK_BASENAME_PREFIX}_x86_64.apk" \
|
||||
"${APK_BASENAME_PREFIX}_x86.apk" \
|
||||
> sha256sums); then
|
||||
exit_on_error "Generate sha25sums failed for '$RELEASE_VERSION_NAME' release."
|
||||
> "${APK_BASENAME_PREFIX}_sha256sums"); then
|
||||
exit_on_error "Generate sha25sums failed for '$APK_VERSION_TAG' release."
|
||||
fi
|
||||
|
||||
- name: Attach universal APK file
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ env.APK_BASENAME_PREFIX }}_universal
|
||||
path: |
|
||||
|
|
@ -73,7 +93,7 @@ jobs:
|
|||
${{ env.APK_DIR_PATH }}/output-metadata.json
|
||||
|
||||
- name: Attach arm64-v8a APK file
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ env.APK_BASENAME_PREFIX }}_arm64-v8a
|
||||
path: |
|
||||
|
|
@ -81,7 +101,7 @@ jobs:
|
|||
${{ env.APK_DIR_PATH }}/output-metadata.json
|
||||
|
||||
- name: Attach armeabi-v7a APK file
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ env.APK_BASENAME_PREFIX }}_armeabi-v7a
|
||||
path: |
|
||||
|
|
@ -89,7 +109,7 @@ jobs:
|
|||
${{ env.APK_DIR_PATH }}/output-metadata.json
|
||||
|
||||
- name: Attach x86_64 APK file
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ env.APK_BASENAME_PREFIX }}_x86_64
|
||||
path: |
|
||||
|
|
@ -97,7 +117,7 @@ jobs:
|
|||
${{ env.APK_DIR_PATH }}/output-metadata.json
|
||||
|
||||
- name: Attach x86 APK file
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ env.APK_BASENAME_PREFIX }}_x86
|
||||
path: |
|
||||
|
|
@ -105,9 +125,9 @@ jobs:
|
|||
${{ env.APK_DIR_PATH }}/output-metadata.json
|
||||
|
||||
- name: Attach sha256sums file
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: sha256sums
|
||||
name: ${{ env.APK_BASENAME_PREFIX }}_sha256sums
|
||||
path: |
|
||||
${{ env.APK_DIR_PATH }}/sha256sums
|
||||
${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_sha256sums
|
||||
${{ env.APK_DIR_PATH }}/output-metadata.json
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
name: Automatic Dependency Submission
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ 'master' ]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
dependency-submission:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: 17
|
||||
- name: Generate and submit dependency graph
|
||||
uses: gradle/actions/dependency-submission@v5
|
||||
|
|
@ -15,5 +15,5 @@ jobs:
|
|||
name: "Validation"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: gradle/wrapper-validation-action@v1
|
||||
- uses: actions/checkout@v6
|
||||
- uses: gradle/actions/wrapper-validation@5
|
||||
|
|
|
|||
|
|
@ -15,7 +15,12 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup java 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
- name: Execute tests
|
||||
run: |
|
||||
./gradlew test
|
||||
|
|
|
|||
|
|
@ -11,9 +11,6 @@ release/
|
|||
.cxx
|
||||
*.zip
|
||||
|
||||
# Crashlytics configuations
|
||||
com_crashlytics_export_strings.xml
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
|
|
@ -27,6 +24,10 @@ local.properties
|
|||
.idea/
|
||||
*.iml
|
||||
|
||||
# Vim
|
||||
*.swo
|
||||
*.swp
|
||||
|
||||
# OS-specific files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
|
|
|
|||
|
|
@ -2,5 +2,5 @@ The `termux/termux-app` repository is released under [GPLv3 only](https://www.gn
|
|||
|
||||
### Exceptions
|
||||
|
||||
- [Terminal Emulator for Android](https://github.com/jackpal/Android-Terminal-Emulator) code is used which is released under [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). Check [`terminal-view`](terminal-view) and [`terminal-emulator`](terminal-emulator) libraries.
|
||||
- [Terminal Emulator for Android](https://github.com/jackpal/Android-Terminal-Emulator) code is used which is released under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) license. Check [`terminal-view`](terminal-view) and [`terminal-emulator`](terminal-emulator) libraries.
|
||||
- Check [`termux-shared/LICENSE.md`](termux-shared/LICENSE.md) for `termux-shared` library related exceptions.
|
||||
|
|
|
|||
165
README.md
165
README.md
|
|
@ -13,22 +13,23 @@ Note that this repository is for the app itself (the user interface and the term
|
|||
|
||||
Quick how-to about Termux package management is available at [Package Management](https://github.com/termux/termux-packages/wiki/Package-Management). It also has info on how to fix **`repository is under maintenance or down`** errors when running `apt` or `pkg` commands.
|
||||
|
||||
***
|
||||
|
||||
**@termux is looking for Termux Application maintainers for implementing new features, fixing bugs and reviewing pull requests since the current one (@fornwall) is inactive.**
|
||||
|
||||
Issue https://github.com/termux/termux-app/issues/1072 needs extra attention.
|
||||
**We are looking for Termux Android application maintainers.**
|
||||
|
||||
***
|
||||
|
||||
### Contents
|
||||
- [Termux App and Plugins](#Termux-App-and-Plugins)
|
||||
- [Installation](#Installation)
|
||||
- [Uninstallation](#Uninstallation)
|
||||
- [Important Links](#Important-Links)
|
||||
- [Debugging](#Debugging)
|
||||
- [For Maintainers and Contributors](#For-Maintainers-and-Contributors)
|
||||
- [Forking](#Forking)
|
||||
**NOTICE: Termux may be unstable on Android 12+.** Android OS will kill any (phantom) processes greater than 32 (limit is for all apps combined) and also kill any processes using excessive CPU. You may get `[Process completed (signal 9) - press Enter]` message in the terminal without actually exiting the shell process yourself. Check the related issue [#2366](https://github.com/termux/termux-app/issues/2366), [issue tracker](https://issuetracker.google.com/u/1/issues/205156966), [phantom cached and empty processes docs](https://github.com/agnostic-apollo/Android-Docs/blob/master/en/docs/apps/processes/phantom-cached-and-empty-processes.md) and [this TLDR comment](https://github.com/termux/termux-app/issues/2366#issuecomment-1237468220) on how to disable trimming of phantom and excessive cpu usage processes. A proper docs page will be added later. An option to disable the killing should be available in Android 12L or 13, so upgrade at your own risk if you are on Android 11, specially if you are not rooted.
|
||||
|
||||
***
|
||||
|
||||
## Contents
|
||||
- [Termux App and Plugins](#termux-app-and-plugins)
|
||||
- [Installation](#installation)
|
||||
- [Uninstallation](#uninstallation)
|
||||
- [Important Links](#important-links)
|
||||
- [Debugging](#debugging)
|
||||
- [For Maintainers and Contributors](#for-maintainers-and-contributors)
|
||||
- [Forking](#forking)
|
||||
- [Sponsors and Funders](#sponsors-and-funders)
|
||||
##
|
||||
|
||||
|
||||
|
|
@ -49,13 +50,17 @@ The core [Termux](https://github.com/termux/termux-app) app comes with the follo
|
|||
|
||||
## Installation
|
||||
|
||||
Latest version is `v0.118.0`.
|
||||
Latest version is `v0.118.3`.
|
||||
|
||||
Termux can be obtained through various sources listed below for **only** Android `>= 7`. Support was dropped for Android `5` and `6` on [2020-01-01](https://www.reddit.com/r/termux/comments/dnzdbs/end_of_android56_support_on_20200101/) at `v0.83`, old builds are available on [archive.org](https://archive.org/details/termux-repositories-legacy).
|
||||
**NOTICE: It is highly recommended that you update to `v0.118.0` or higher ASAP for various bug fixes, including a critical world-readable vulnerability reported [here](https://termux.github.io/general/2022/02/15/termux-apps-vulnerability-disclosures.html). See [below](#google-play-store-experimental-branch) for information regarding Termux on Google Play.**
|
||||
|
||||
The APK files of different sources are signed with different signature keys. The `Termux` app and all its plugins use the same [`sharedUserId`](https://developer.android.com/guide/topics/manifest/manifest-element) `com.termux` and so all their APKs installed on a device must have been signed with the same signature key to work together and so they must all be installed from the same source. Do not attempt to mix them together, i.e do not try to install an app or plugin from `F-Droid` and another one from a different source like `Github`. Android Package Manager will also normally not allow installation of APKs with different signatures and you will get errors on installation like `App not installed`, `Failed to install due to an unknown error`, `INSTALL_FAILED_UPDATE_INCOMPATIBLE`, `INSTALL_FAILED_SHARED_USER_INCOMPATIBLE`, `signatures do not match previously installed version`, etc. This restriction can be bypassed with root or with custom roms.
|
||||
Termux can be obtained through various sources listed below for **only** Android `>= 7` with full support for apps and packages.
|
||||
|
||||
If you wish to install from a different source, then you must **uninstall any and all existing Termux or its plugin app APKs** from your device first, then install all new APKs from the same new source. Check [Uninstallation](#Uninstallation) section for details. You may also want to consider [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux) before the uninstallation so that you can restore it after re-installing from Termux different source.
|
||||
Support for both app and packages was dropped for Android `5` and `6` on [2020-01-01](https://www.reddit.com/r/termux/comments/dnzdbs/end_of_android56_support_on_20200101/) at `v0.83`, however it was re-added just for the app *without any support for package updates* on [2022-05-24](https://github.com/termux/termux-app/pull/2740) via the [GitHub](#github) sources. Check [here](https://github.com/termux/termux-app/wiki/Termux-on-android-5-or-6) for the details.
|
||||
|
||||
The APK files of different sources are signed with different signature keys. The `Termux` app and all its plugins use the same [`sharedUserId`](https://developer.android.com/guide/topics/manifest/manifest-element) `com.termux` and so all their APKs installed on a device must have been signed with the same signature key to work together and so they must all be installed from the same source. Do not attempt to mix them together, i.e do not try to install an app or plugin from `F-Droid` and another one from a different source like `GitHub`. Android Package Manager will also normally not allow installation of APKs with different signatures and you will get errors on installation like `App not installed`, `Failed to install due to an unknown error`, `INSTALL_FAILED_UPDATE_INCOMPATIBLE`, `INSTALL_FAILED_SHARED_USER_INCOMPATIBLE`, `signatures do not match previously installed version`, etc. This restriction can be bypassed with root or with custom roms.
|
||||
|
||||
If you wish to install from a different source, then you must **uninstall any and all existing Termux or its plugin app APKs** from your device first, then install all new APKs from the same new source. Check [Uninstallation](#uninstallation) section for details. You may also want to consider [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux) before the uninstallation so that you can restore it after re-installing from Termux different source.
|
||||
|
||||
In the following paragraphs, *"bootstrap"* refers to the minimal packages that are shipped with the `termux-app` itself to start a working shell environment. Its zips are built and released [here](https://github.com/termux/termux-packages/releases).
|
||||
|
||||
|
|
@ -65,67 +70,64 @@ Termux application can be obtained from `F-Droid` from [here](https://f-droid.or
|
|||
|
||||
You **do not** need to download the `F-Droid` app (via the `Download F-Droid` link) to install Termux. You can download the Termux APK directly from the site by clicking the `Download APK` link at the bottom of each version section.
|
||||
|
||||
It usually takes a few days (or even a week or more) for updates to be available on `F-Droid` once an update has been released on `Github`. The `F-Droid` releases are built and published by `F-Droid` once they [detect](https://gitlab.com/fdroid/fdroiddata/-/blob/master/metadata/com.termux.yml) a new `Github` release. The Termux maintainers **do not** have any control over the building and publishing of the Termux apps on `F-Droid`. Moreover, the Termux maintainers also do not have access to the APK signing keys of `F-Droid` releases, so we cannot release an APK ourselves on `Github` that would be compatible with `F-Droid` releases.
|
||||
It usually takes a few days (or even a week or more) for updates to be available on `F-Droid` once an update has been released on `GitHub`. The `F-Droid` releases are built and published by `F-Droid` once they [detect](https://gitlab.com/fdroid/fdroiddata/-/blob/master/metadata/com.termux.yml) a new `GitHub` release. The Termux maintainers **do not** have any control over the building and publishing of the Termux apps on `F-Droid`. Moreover, the Termux maintainers also do not have access to the APK signing keys of `F-Droid` releases, so we cannot release an APK ourselves on `GitHub` that would be compatible with `F-Droid` releases.
|
||||
|
||||
The `F-Droid` app often may not notify you of updates and you will manually have to do a pull down swipe action in the `Updates` tab of the app for it to check updates. Make sure battery optimizations are disabled for the app, check https://dontkillmyapp.com/ for details on how to do that.
|
||||
|
||||
Only a universal APK is released, which will work on all supported architectures. The APK and bootstrap installation size will be `~180MB`. `F-Droid` does [not support](https://github.com/termux/termux-app/pull/1904) architecture specific APKs.
|
||||
|
||||
### Github
|
||||
### GitHub
|
||||
|
||||
Termux application can be obtained on `Github` either from [`Github Releases`](https://github.com/termux/termux-app/releases) for version `>= 0.118.0` or from [`Github Build`](https://github.com/termux/termux-app/actions/workflows/debug_build.yml) action workflows.
|
||||
Termux application can be obtained on `GitHub` either from [`GitHub Releases`](https://github.com/termux/termux-app/releases) for version `>= 0.118.0` or from [`GitHub Build Action`](https://github.com/termux/termux-app/actions/workflows/debug_build.yml?query=branch%3Amaster+event%3Apush) workflows. **For android `>= 7`, only install `apt-android-7` variants. For android `5` and `6`, only install `apt-android-5` variants.**
|
||||
|
||||
The APKs for `Github Releases` will be listed under `Assets` drop-down of a release. These are automatically attached when a new version is released.
|
||||
The APKs for `GitHub Releases` will be listed under `Assets` drop-down of a release. These are automatically attached when a new version is released.
|
||||
|
||||
The APKs for `Github Build` action workflows will be listed under `Artifacts` section of a workflow run. These are created for each commit/push done to the repository and can be used by users who don't want to wait for releases and want to try out the latest features immediately or want to test their pull requests. Note that for action workflows, you need to be [**logged into a `Github` account**](https://github.com/login) for the `Artifacts` links to be enabled/clickable. If you are using the [`Github` app](https://github.com/mobile), then make sure to open workflow link in a browser like Chrome or Firefox that has your Github account logged in since the in-app browser may not be logged in.
|
||||
The APKs for `GitHub Build` action workflows will be listed under `Artifacts` section of a workflow run. These are created for each commit/push done to the repository and can be used by users who don't want to wait for releases and want to try out the latest features immediately or want to test their pull requests. Note that for action workflows, you need to be [**logged into a `GitHub` account**](https://github.com/login) for the `Artifacts` links to be enabled/clickable. If you are using the [`GitHub` app](https://github.com/mobile), then make sure to open workflow link in a browser like Chrome or Firefox that has your GitHub account logged in since the in-app browser may not be logged in.
|
||||
|
||||
The APKs for both of these are [`debuggable`](https://developer.android.com/studio/debug) and are compatible with each other but they are not compatible with other sources.
|
||||
|
||||
Both universal and architecture specific APKs are released. The APK and bootstrap installation size will be `~180MB` if using universal and `~120MB` if using architecture specific. Check [here](https://github.com/termux/termux-app/issues/2153) for details.
|
||||
|
||||
### Google Play Store **(Deprecated)**
|
||||
**Security warning**: APK files on GitHub are signed with a test key that has been [shared with community](https://github.com/termux/termux-app/blob/master/app/testkey_untrusted.jks). This IS NOT an official developer key and everyone can use it to generate releases for own testing. Be very careful when using Termux GitHub builds obtained elsewhere except https://github.com/termux/termux-app. Everyone is able to use it to forge a malicious Termux update installable over the GitHub build. Think twice about installing Termux builds distributed via Telegram or other social media. If your device get caught by malware, we will not be able to help you.
|
||||
|
||||
**Termux and its plugins are no longer updated on [Google Play Store](https://play.google.com/store/apps/details?id=com.termux) due to [android 10 issues](https://github.com/termux/termux-packages/wiki/Termux-and-Android-10) and have been deprecated.** The last version released for Android `>= 7` was `v0.101`. **It is highly recommended to not install Termux apps from Play Store any more.**
|
||||
|
||||
There are plans for **unpublishing** the Termux app and all its plugins on Play Store soon so that new users cannot install it and for **disabling** the Termux apps with updates so that existing users **cannot continue using outdated versions**. You are encouraged to move to `F-Droid` or `Github` builds as soon as possible.
|
||||
|
||||
You **will not need to buy plugins again** if you bought them on Play Store. All plugins are free on `F-Droid` and `Github`.
|
||||
|
||||
You can backup all your data under `$HOME/` and `$PREFIX/` before changing installation source, and then restore it afterwards, by following instructions at [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux) before the uninstallation.
|
||||
|
||||
There is currently no work being done to solve android `10` issues and *working* updates will not be resumed on Google Play Store any time soon. We will continue targeting sdk `28` for now. So there is not much point in staying on Play Store builds and waiting for updates to be resumed. If for some reason you don't want to move to `F-Droid` or `Github` sources for now, then at least check [Package Management](https://github.com/termux/termux-packages/wiki/Package-Management) to **change your mirror**, otherwise, you will get **`repository is under maintenance or down`** errors when running `apt` or `pkg` commands. After that, it is also **highly advisable** to run `pkg upgrade` command to update all packages to the latest available versions, or at least update `termux-tools` package with `pkg install termux-tools` command.
|
||||
|
||||
Note that by upgrading old packages to latest versions, like that of `python` may break your setups/scripts since they may not be compatible anymore. Moreover, you will not be able to downgrade the package versions since termux repos only keep the latest version and you will have to manually rebuild the old versions of the packages if required as per https://github.com/termux/termux-packages/wiki/Building-packages.
|
||||
|
||||
If you plan on staying on Play Store sources in future as well, then you may want to **disable automatic updates in Play Store** for Termux apps, since if and when updates to disable Termux apps are released, then **you will not be able to downgrade** and **will be forced** to move since apps won't work anymore. Only a way to backup `termux-app` data may be provided. The `termux-tools` [version `>= 0.135`](https://github.com/termux/termux-packages/pull/7493) will also show a banner at the top of the terminal saying `You are likely using a very old version of Termux, probably installed from the Google Play Store.`, you can remove it by running `rm -f /data/data/com.termux/files/usr/etc/motd-playstore` and restarting the app.
|
||||
|
||||
#### Why Disable?
|
||||
The [test key](https://github.com/termux/termux-app/blob/master/app/testkey_untrusted.jks) shall not be used to impersonate @termux and can't be used for this anyway. This key is not trusted by us and it is quite easy to detect its use in user generated content.
|
||||
|
||||
<details>
|
||||
<summary></summary>
|
||||
<summary>Keystore information</summary>
|
||||
|
||||
- They should be disabled because deprecated things get removed and are not supported after some time, its the standard practice. It has been many months now since deprecation was announced and updates have not been released on Play Store since after `29 September 2020`.
|
||||
```
|
||||
Alias name: alias
|
||||
Creation date: Oct 4, 2019
|
||||
Entry type: PrivateKeyEntry
|
||||
Certificate chain length: 1
|
||||
Certificate[1]:
|
||||
Owner: CN=APK Signer, OU=Earth, O=Earth
|
||||
Issuer: CN=APK Signer, OU=Earth, O=Earth
|
||||
Serial number: 29be297b
|
||||
Valid from: Wed Sep 04 02:03:24 EEST 2019 until: Tue Oct 26 02:03:24 EEST 2049
|
||||
Certificate fingerprints:
|
||||
SHA1: 51:79:55:EA:BF:69:FC:05:7C:41:C7:D3:79:DB:BC:EF:20:AD:85:F2
|
||||
SHA256: B6:DA:01:48:0E:EF:D5:FB:F2:CD:37:71:B8:D1:02:1E:C7:91:30:4B:DD:6C:4B:F4:1D:3F:AA:BA:D4:8E:E5:E1
|
||||
Signature algorithm name: SHA1withRSA (disabled)
|
||||
Subject Public Key Algorithm: 2048-bit RSA key
|
||||
Version: 3
|
||||
```
|
||||
|
||||
- The new versions have lots of **new features and fixes** which you can mostly check out in the Changelog of [`Github Releases`](https://github.com/termux/termux-app/releases) that you may be missing out. Extra detail is usually provided in [commit messages](https://github.com/termux/termux-app/commits/master).
|
||||
|
||||
- Users on old versions are quite often reporting issues in multiple repositories and support forums that were **fixed months ago**, which we then have to deal with. The maintainers of @termux work in their free time, majorly for free, to work on development and provide support and having to re-re-deal with old issues takes away the already limited time from current work and is not possible to continue doing. Play Store page of `termux-app` has been filled with bad reviews of *"broken app"*, even though its clearly mentioned on the page that app is not being updated, yet users don't read and still install and report issues.
|
||||
|
||||
- Asking people to pay for plugins when the `termux-app` at installation time is broken due to repository issues and has bugs is unethical.
|
||||
|
||||
- Old versions don't have proper logging/debugging and crash report support. Reporting bugs without logs or detailed info is not helpful in solving them.
|
||||
|
||||
- It's also easier for us to solve package related issues and provide custom functionality with app updates, which can't be done if users continue using old versions. For example, the [bintray shudown](https://github.com/termux/termux-packages/wiki/Package-Management) causing package install/update failures for new Play Store users is/was not an issue for F-Droid users since it is being shipped with updated bootstrap and repo info, hence no reported issues from new F-Droid users.
|
||||
</details>
|
||||
|
||||
##
|
||||
### Google Play Store **(Experimental branch)**
|
||||
|
||||
There is currently a build of Termux available on Google Play for Android 11+ devices, with extensive adjustments in order to pass policy requirements there. This is under development and has missing functionality and bugs (see [here](https://github.com/termux-play-store/) for status updates) compared to the stable F-Droid build, which is why most users who can should still use F-Droid or GitHub build as mentioned above.
|
||||
|
||||
Currently, Google Play will try to update installations away from F-Droid ones. Updating will still fail as [sharedUserId](https://developer.android.com/guide/topics/manifest/manifest-element#uid) has been removed. A planned 0.118.1 F-Droid release will fix this by setting a higher version code than used for the PlayStore app. Meanwhile, to prevent Google Play from attempting to download and then fail to install the Google Play releases over existing installations, you can open the Termux apps pages on Google Play and then click on the 3 dots options button in the top right and then disable the Enable auto update toggle. However, the Termux apps updates will still show in the PlayStore app updates list.
|
||||
|
||||
If you want to help out with testing the Google Play build (or cannot install Termux from other sources), be aware that it's built from a separate repository (https://github.com/termux-play-store/) - be sure to report issues [there](https://github.com/termux-play-store/termux-issues/issues/new/choose), as any issues encountered might very well be specific to that repository.
|
||||
|
||||
## Uninstallation
|
||||
|
||||
Uninstallation may be required if a user doesn't want Termux installed in their device anymore or is switching to a different [install source](#Installation). You may also want to consider [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux) before the uninstallation.
|
||||
Uninstallation may be required if a user doesn't want Termux installed in their device anymore or is switching to a different [install source](#installation). You may also want to consider [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux) before the uninstallation.
|
||||
|
||||
To uninstall Termux completely, you must uninstall **any and all existing Termux or its plugin app APKs** listed in [Termux App and Plugins](#Termux-App-and-Plugins).
|
||||
To uninstall Termux completely, you must uninstall **any and all existing Termux or its plugin app APKs** listed in [Termux App and Plugins](#termux-app-and-plugins).
|
||||
|
||||
Go to `Android Settings` -> `Applications` and then look for those apps. You can also use the search feature if it’s available on your device and search `termux` in the applications list.
|
||||
|
||||
|
|
@ -142,10 +144,10 @@ All community links are available [here](https://wiki.termux.com/wiki/Community)
|
|||
The main ones are the following.
|
||||
|
||||
- [Termux Reddit community](https://reddit.com/r/termux)
|
||||
- [Termux Matrix Channel](https://matrix.to/#termux_termux:gitter.im)
|
||||
- [Termux Dev Matrix Channel](https://matrix.to/#termux_dev:gitter.im)
|
||||
- [Termux Twitter](https://twitter.com/termux/)
|
||||
- [Termux Reports Email](mailto:termuxreports@groups.io)
|
||||
- [Termux User Matrix Channel](https://matrix.to/#/#termux_termux:gitter.im) ([Gitter](https://gitter.im/termux/termux))
|
||||
- [Termux Dev Matrix Channel](https://matrix.to/#/#termux_dev:gitter.im) ([Gitter](https://gitter.im/termux/dev))
|
||||
- [Termux X (Twitter)](https://twitter.com/termuxdevs)
|
||||
- [Termux Support Email](mailto:support@termux.dev)
|
||||
|
||||
### Wikis
|
||||
|
||||
|
|
@ -231,7 +233,21 @@ The main Termux constants are defined by [`TermuxConstants`](https://github.com/
|
|||
|
||||
Check [Termux Libraries](https://github.com/termux/termux-app/wiki/Termux-Libraries) for how to import termux libraries in plugin apps and [Forking and Local Development](https://github.com/termux/termux-app/wiki/Termux-Libraries#forking-and-local-development) for how to update termux libraries for plugins.
|
||||
|
||||
Commit messages **must** use [Conventional Commits](https://www.conventionalcommits.org) specs so that chagelogs can automatically be generated by the [`create-conventional-changelog`](https://github.com/termux/create-conventional-changelog) script, check its repo for further details on the spec. Use the following `types` as `Added: Add foo`, `Added|Fixed: Add foo and fix bar`, `Changed!: Change baz as a breaking change`, etc. You can optionally add a scope as well, like `Fixed(terminal): Some bug`. The space after `:` is necessary.
|
||||
The `versionName` in `build.gradle` files of Termux and its plugin apps must follow the [semantic version `2.0.0` spec](https://semver.org/spec/v2.0.0.html) in the format `major.minor.patch(-prerelease)(+buildmetadata)`. When bumping `versionName` in `build.gradle` files and when creating a tag for new releases on GitHub, make sure to include the patch number as well, like `v0.1.0` instead of just `v0.1`. The `build.gradle` files and `attach_debug_apks_to_release` workflow validates the version as well and the build/attachment will fail if `versionName` does not follow the spec.
|
||||
|
||||
### Commit Messages Guidelines
|
||||
|
||||
Commit messages **must** use the [Conventional Commits](https://www.conventionalcommits.org) spec so that chagelogs as per the [Keep a Changelog](https://github.com/olivierlacan/keep-a-changelog) spec can automatically be generated by the [`create-conventional-changelog`](https://github.com/termux/create-conventional-changelog) script, check its repo for further details on the spec. **The first letter for `type` and `description` must be capital and description should be in the present tense.** The space after the colon `:` is necessary. For a breaking change, add an exclamation mark `!` before the colon `:`, so that it is highlighted in the chagelog automatically.
|
||||
|
||||
```
|
||||
<type>[optional scope]: <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer(s)]
|
||||
```
|
||||
|
||||
**Only the `types` listed below must be used exactly as they are used in the changelog headings.** For example, `Added: Add foo`, `Added|Fixed: Add foo and fix bar`, `Changed!: Change baz as a breaking change`, etc. You can optionally add a scope as well, like `Fixed(terminal): Fix some bug`. **Do not use anything else as type, like `add` instead of `Added`, etc.**
|
||||
|
||||
- **Added** for new features.
|
||||
- **Changed** for changes in existing functionality.
|
||||
|
|
@ -239,11 +255,6 @@ Commit messages **must** use [Conventional Commits](https://www.conventionalcomm
|
|||
- **Removed** for now removed features.
|
||||
- **Fixed** for any bug fixes.
|
||||
- **Security** in case of vulnerabilities.
|
||||
- **Docs** for updating documentation.
|
||||
|
||||
Changelogs for releases are generated based on [Keep a Changelog](https://github.com/olivierlacan/keep-a-changelog) specs.
|
||||
|
||||
The `versionName` in `build.gradle` files of Termux and its plugin apps must follow the [semantic version `2.0.0` spec](https://semver.org/spec/v2.0.0.html) in the format `major.minor.patch(-prerelease)(+buildmetadata)`. When bumping `versionName` in `build.gradle` files and when creating a tag for new releases on github, make sure to include the patch number as well, like `v0.1.0` instead of just `v0.1`. The `build.gradle` files and `attach_debug_apks_to_release` workflow validates the version as well and the build/attachment will fail if `versionName` does not follow the spec.
|
||||
##
|
||||
|
||||
|
||||
|
|
@ -251,6 +262,34 @@ The `versionName` in `build.gradle` files of Termux and its plugin apps must fol
|
|||
## Forking
|
||||
|
||||
- Check [`TermuxConstants`](https://github.com/termux/termux-app/blob/master/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java) javadocs for instructions on what changes to make in the app to change package name.
|
||||
- You also need to recompile bootstrap zip for the new package name. Check [here](https://github.com/termux/termux-app/issues/1983) and [here](https://github.com/termux/termux-app/issues/2081#issuecomment-865280111) for experimental work on it.
|
||||
- You also need to recompile bootstrap zip for the new package name. Check [building bootstrap](https://github.com/termux/termux-packages/wiki/For-maintainers#build-bootstrap-archives), [here](https://github.com/termux/termux-app/issues/1983) and [here](https://github.com/termux/termux-app/issues/2081#issuecomment-865280111).
|
||||
- Currently, not all plugins use `TermuxConstants` from `termux-shared` library and have hardcoded `com.termux` values and will need to be manually patched.
|
||||
- If forking termux plugins, check [Forking and Local Development](https://github.com/termux/termux-app/wiki/Termux-Libraries#forking-and-local-development) for info on how to use termux libraries for plugins.
|
||||
##
|
||||
|
||||
|
||||
|
||||
## Sponsors and Funders
|
||||
|
||||
[<img alt="GitHub Accelerator" width="25%" src="site/assets/sponsors/github.png" />](https://github.com)
|
||||
*[GitHub Accelerator](https://github.com/accelerator) ([1](https://github.blog/2023-04-12-github-accelerator-our-first-cohort-and-whats-next))*
|
||||
|
||||
|
||||
|
||||
[<img alt="GitHub Secure Open Source Fund" width="25%" src="site/assets/sponsors/github.png" />](https://github.com)
|
||||
*[GitHub Secure Open Source Fund](https://resources.github.com/github-secure-open-source-fund) ([1](https://github.blog/open-source/maintainers/securing-the-supply-chain-at-scale-starting-with-71-important-open-source-projects), [2](https://termux.dev/en/posts/general/2025/08/11/termux-selected-for-github-secure-open-source-fund-session-2.html))*
|
||||
|
||||
|
||||
|
||||
[<img alt="NLnet NGI Mobifree" width="25%" src="site/assets/sponsors/nlnet-ngi-mobifree.png" />](https://nlnet.nl/mobifree)
|
||||
*[NLnet NGI Mobifree](https://nlnet.nl/mobifree) ([1](https://nlnet.nl/news/2024/20241111-NGI-Mobifree-grants.html), [2](https://termux.dev/en/posts/general/2024/11/11/termux-selected-for-nlnet-ngi-mobifree-grant.html))*
|
||||
|
||||
|
||||
|
||||
[<img alt="Cloudflare" width="25%" src="site/assets/sponsors/cloudflare.png" />](https://www.cloudflare.com)
|
||||
*[Cloudflare](https://www.cloudflare.com) ([1](https://packages-cf.termux.dev))*
|
||||
|
||||
|
||||
|
||||
[<img alt="Warp" width="25%" src="https://github.com/warpdotdev/brand-assets/blob/640dffd347439bbcb535321ab36b7281cf4446c0/Github/Sponsor/Warp-Github-LG-03.png" />](https://www.warp.dev/?utm_source=github&utm_medium=readme&utm_campaign=termux)
|
||||
[*Warp, built for coding with multiple AI agents*](https://www.warp.dev/?utm_source=github&utm_medium=readme&utm_campaign=termux)
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
Check https://termux.dev/security for info on Termux security policies and how to report vulnerabilities.
|
||||
|
|
@ -2,7 +2,19 @@ plugins {
|
|||
id "com.android.application"
|
||||
}
|
||||
|
||||
ext {
|
||||
// The packageVariant defines the bootstrap variant that will be included in the app APK.
|
||||
// This must be supported by com.termux.shared.termux.TermuxBootstrap.PackageVariant or app will
|
||||
// crash at startup.
|
||||
// Bootstrap of a different variant must not be manually installed by the user after app installation
|
||||
// by replacing $PREFIX since app code is dependant on the variant used to build the APK.
|
||||
// Currently supported values are: [ "apt-android-7" "apt-android-5" ]
|
||||
packageVariant = System.getenv("TERMUX_PACKAGE_VARIANT") ?: "apt-android-7" // Default: "apt-android-7"
|
||||
}
|
||||
|
||||
android {
|
||||
namespace "com.termux"
|
||||
|
||||
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
||||
ndkVersion = System.getenv("JITPACK_NDK_VERSION") ?: project.properties.ndkVersion
|
||||
def appVersionName = System.getenv("TERMUX_APP_VERSION_NAME") ?: ""
|
||||
|
|
@ -11,23 +23,24 @@ android {
|
|||
def splitAPKsForReleaseBuilds = System.getenv("TERMUX_SPLIT_APKS_FOR_RELEASE_BUILDS") ?: "0" // F-Droid does not support split APKs #1904
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.annotation:annotation:1.3.0"
|
||||
implementation "androidx.core:core:1.6.0"
|
||||
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
|
||||
implementation "androidx.preference:preference:1.1.1"
|
||||
implementation "androidx.annotation:annotation:1.9.0"
|
||||
implementation "androidx.core:core:1.13.1"
|
||||
implementation "androidx.drawerlayout:drawerlayout:1.2.0"
|
||||
implementation "androidx.preference:preference:1.2.1"
|
||||
implementation "androidx.viewpager:viewpager:1.0.0"
|
||||
implementation "com.google.android.material:material:1.12.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 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
|
||||
|
||||
implementation project(":terminal-view")
|
||||
implementation project(":termux-shared")
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.termux"
|
||||
minSdkVersion project.properties.minSdkVersion.toInteger()
|
||||
targetSdkVersion project.properties.targetSdkVersion.toInteger()
|
||||
versionCode 118
|
||||
|
|
@ -36,6 +49,8 @@ android {
|
|||
if (appVersionName) versionName = appVersionName
|
||||
validateVersionName(versionName)
|
||||
|
||||
buildConfigField "String", "TERMUX_PACKAGE_VARIANT", "\"" + project.ext.packageVariant + "\"" // Used by TermuxApplication class
|
||||
|
||||
manifestPlaceholders.TERMUX_PACKAGE_NAME = "com.termux"
|
||||
manifestPlaceholders.TERMUX_APP_NAME = "Termux"
|
||||
manifestPlaceholders.TERMUX_API_APP_NAME = "Termux:API"
|
||||
|
|
@ -64,7 +79,7 @@ android {
|
|||
|
||||
signingConfigs {
|
||||
debug {
|
||||
storeFile file('dev_keystore.jks')
|
||||
storeFile file('testkey_untrusted.jks')
|
||||
keyAlias 'alias'
|
||||
storePassword 'xrj45yWGLbsO7W0v'
|
||||
keyPassword 'xrj45yWGLbsO7W0v'
|
||||
|
|
@ -84,6 +99,9 @@ android {
|
|||
}
|
||||
|
||||
compileOptions {
|
||||
// Flag to enable support for the new language APIs
|
||||
coreLibraryDesugaringEnabled true
|
||||
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
|
@ -94,7 +112,7 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
lint {
|
||||
disable 'ProtectedPermissions'
|
||||
}
|
||||
|
||||
|
|
@ -114,19 +132,23 @@ android {
|
|||
variant.outputs.all { output ->
|
||||
if (variant.buildType.name == "debug") {
|
||||
def abi = output.getFilter(com.android.build.OutputFile.ABI)
|
||||
outputFileName = new File("termux-app_" + (apkVersionTag ? apkVersionTag : "debug") + "_" + (abi ? abi : "universal") + ".apk")
|
||||
outputFileName = new File("termux-app_" + (apkVersionTag ? apkVersionTag : project.ext.packageVariant + "-" + "debug") + "_" + (abi ? abi : "universal") + ".apk")
|
||||
} else if (variant.buildType.name == "release") {
|
||||
def abi = output.getFilter(com.android.build.OutputFile.ABI)
|
||||
outputFileName = new File("termux-app_" + (apkVersionTag ? apkVersionTag : "release") + "_" + (abi ? abi : "universal") + ".apk")
|
||||
outputFileName = new File("termux-app_" + (apkVersionTag ? apkVersionTag : project.ext.packageVariant + "-" + "release") + "_" + (abi ? abi : "universal") + ".apk")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
testImplementation "org.robolectric:robolectric:4.4"
|
||||
testImplementation "org.robolectric:robolectric:4.10"
|
||||
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.2"
|
||||
}
|
||||
|
||||
task versionName {
|
||||
|
|
@ -160,7 +182,7 @@ def downloadBootstrap(String arch, String expectedChecksum, String version) {
|
|||
if (checksum == expectedChecksum) {
|
||||
return
|
||||
} else {
|
||||
logger.quiet("Deleting old local file with wrong hash: " + localUrl)
|
||||
logger.quiet("Deleting old local file with wrong hash: " + localUrl + ": expected: " + expectedChecksum + ", actual: " + checksum)
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
|
|
@ -195,11 +217,22 @@ clean {
|
|||
|
||||
task downloadBootstraps() {
|
||||
doLast {
|
||||
def version = "2022.01.07-r1"
|
||||
downloadBootstrap("aarch64", "0fe6d0159d12fcb8baf7750ce9072b9b36f742662b02ad4da145ab85873614cd", version)
|
||||
downloadBootstrap("arm", "0a6014e2ec3b7079524fee3caabd02be05bcb4add3c6cd9e5ad98408b428c717", version)
|
||||
downloadBootstrap("i686", "c55369a9af1316dc3d99457aa23cce64b29c1e60e375159352c0d4b9cfed4ac6", version)
|
||||
downloadBootstrap("x86_64", "e93a7c66d15edb7d1b5ceca906255c85ead35a8b6a52f93524e117cfb07e6778", version)
|
||||
def packageVariant = project.ext.packageVariant
|
||||
if (packageVariant == "apt-android-7") {
|
||||
def version = "2022.04.28-r5" + "+" + packageVariant
|
||||
downloadBootstrap("aarch64", "4a51a7eb209fe82efc24d52e3cccc13165f27377290687cb82038cbd8e948430", version)
|
||||
downloadBootstrap("arm", "6459a786acbae50d4c8a36fa1c3de6a4dd2d482572f6d54f73274709bd627325", version)
|
||||
downloadBootstrap("i686", "919d212b2f19e08600938db4079e794e947365022dbfd50ac342c50fcedcd7be", version)
|
||||
downloadBootstrap("x86_64", "61b02fdc03ea4f5d9da8d8cf018013fdc6659e6da6cbf44e9b24d1c623580b89", version)
|
||||
} else if (packageVariant == "apt-android-5") {
|
||||
def version = "2022.04.28-r6" + "+" + packageVariant
|
||||
downloadBootstrap("aarch64", "913609d439415c828c5640be1b0561467e539cb1c7080662decaaca2fb4820e7", version)
|
||||
downloadBootstrap("arm", "26bfb45304c946170db69108e5eb6e3641aad751406ce106c80df80cad2eccf8", version)
|
||||
downloadBootstrap("i686", "46dcfeb5eef67ba765498db9fe4c50dc4690805139aa0dd141a9d8ee0693cd27", version)
|
||||
downloadBootstrap("x86_64", "615b590679ee6cd885b7fd2ff9473c845e920f9b422f790bb158c63fe42b8481", version)
|
||||
} else {
|
||||
throw new GradleException("Unsupported TERMUX_PACKAGE_VARIANT \"" + packageVariant + "\"")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,3 @@
|
|||
-dontobfuscate
|
||||
#-renamesourcefileattribute SourceFile
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# Temp fix for androidx.window:window:1.0.0-alpha09 imported by termux-shared
|
||||
# https://issuetracker.google.com/issues/189001730
|
||||
# https://android-review.googlesource.com/c/platform/frameworks/support/+/1757630
|
||||
-keep class androidx.window.** { *; }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.termux"
|
||||
android:installLocation="internalOnly"
|
||||
android:sharedUserId="${TERMUX_PACKAGE_NAME}"
|
||||
android:sharedUserLabel="@string/shared_user_label">
|
||||
|
|
@ -22,7 +21,9 @@
|
|||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
|
@ -32,7 +33,9 @@
|
|||
<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" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" tools:ignore="ProtectedPermissions" />
|
||||
<uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
|
||||
|
||||
<application
|
||||
android:name=".app.TermuxApplication"
|
||||
|
|
@ -41,16 +44,21 @@
|
|||
android:extractNativeLibs="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/application_name"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="false"
|
||||
android:theme="@style/Theme.Termux">
|
||||
android:theme="@style/Theme.TermuxApp.DayNight.DarkActionBar"
|
||||
tools:targetApi="m">
|
||||
|
||||
<activity
|
||||
android:name=".app.TermuxActivity"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|uiMode|keyboard|keyboardHidden|navigation"
|
||||
android:exported="true"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|keyboard|keyboardHidden|navigation"
|
||||
android:label="@string/application_name"
|
||||
android:launchMode="singleTask"
|
||||
android:resizeableActivity="true">
|
||||
android:resizeableActivity="true"
|
||||
android:theme="@style/Theme.TermuxActivity.DayNight.NoActionBar"
|
||||
tools:targetApi="n">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
|
|
@ -69,6 +77,7 @@
|
|||
|
||||
<activity-alias
|
||||
android:name=".HomeActivity"
|
||||
android:exported="true"
|
||||
android:targetActivity=".app.TermuxActivity">
|
||||
|
||||
<!-- Launch activity automatically on boot on Android Things devices -->
|
||||
|
|
@ -86,27 +95,33 @@
|
|||
android:label="@string/application_name"
|
||||
android:parentActivityName=".app.TermuxActivity"
|
||||
android:resizeableActivity="true"
|
||||
android:theme="@android:style/Theme.Material.Light.DarkActionBar" />
|
||||
tools:targetApi="n" />
|
||||
|
||||
<activity
|
||||
android:name=".app.activities.SettingsActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/title_activity_termux_settings"
|
||||
android:theme="@style/Theme.AppCompat.Light.DarkActionBar" />
|
||||
android:theme="@style/Theme.TermuxApp.DayNight.NoActionBar" />
|
||||
|
||||
<activity
|
||||
android:name=".shared.activities.ReportActivity"
|
||||
android:theme="@style/Theme.AppCompat.TermuxReportActivity"
|
||||
android:documentLaunchMode="intoExisting"
|
||||
/>
|
||||
android:theme="@style/Theme.MarkdownViewActivity.DayNight"
|
||||
android:documentLaunchMode="intoExisting" />
|
||||
|
||||
<activity
|
||||
android:name=".filepicker.TermuxFileReceiverActivity"
|
||||
android:name=".app.api.file.FileReceiverActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:label="@string/application_name"
|
||||
android:exported="false"
|
||||
android:noHistory="true"
|
||||
android:resizeableActivity="true"
|
||||
android:taskAffinity="${TERMUX_PACKAGE_NAME}.filereceiver">
|
||||
android:taskAffinity="${TERMUX_PACKAGE_NAME}.filereceiver"
|
||||
tools:targetApi="n">
|
||||
</activity>
|
||||
|
||||
<activity-alias
|
||||
android:name=".app.api.file.FileShareReceiverActivity"
|
||||
android:exported="true"
|
||||
android:targetActivity=".app.api.file.FileReceiverActivity">
|
||||
|
||||
<!-- Accept multiple file types when sending. -->
|
||||
<intent-filter>
|
||||
|
|
@ -122,6 +137,13 @@
|
|||
<data android:mimeType="text/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<activity-alias
|
||||
android:name=".app.api.file.FileViewReceiverActivity"
|
||||
android:exported="true"
|
||||
android:targetActivity=".app.api.file.FileReceiverActivity">
|
||||
|
||||
<!-- Accept multiple file types to let Termux be usable as generic file viewer. -->
|
||||
<intent-filter tools:ignore="AppLinkUrlError">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
|
@ -134,8 +156,7 @@
|
|||
<data android:mimeType="text/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</activity-alias>
|
||||
|
||||
<provider
|
||||
android:name=".filepicker.TermuxDocumentsProvider"
|
||||
|
|
@ -156,9 +177,21 @@
|
|||
android:permission="${TERMUX_PACKAGE_NAME}.permission.RUN_COMMAND" />
|
||||
|
||||
|
||||
<receiver android:name=".app.TermuxOpenReceiver" android:exported="false" />
|
||||
<receiver
|
||||
android:name=".app.TermuxOpenReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
<receiver android:name=".shared.activities.ReportActivity$ReportActivityBroadcastReceiver" android:exported="false" />
|
||||
<receiver
|
||||
android:name=".app.event.SystemEventReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".shared.activities.ReportActivity$ReportActivityBroadcastReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
|
||||
<service
|
||||
|
|
@ -180,13 +213,24 @@
|
|||
<meta-data
|
||||
android:name="android.max_aspect"
|
||||
android:value="10.0" />
|
||||
|
||||
|
||||
<!-- https://developer.samsung.com/samsung-dex/modify-optimizing.html -->
|
||||
|
||||
<!-- Version < 3.0. DeX Mode and Screen Mirroring support -->
|
||||
<meta-data
|
||||
android:name="com.sec.android.support.multiwindow"
|
||||
android:name="com.samsung.android.keepalive.density"
|
||||
android:value="true" />
|
||||
|
||||
<!-- Version >= 3.0. DeX Dual Mode support -->
|
||||
<meta-data
|
||||
android:name="com.samsung.android.multidisplay.keep_process_alive"
|
||||
android:value="true" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.sec.android.support.multiwindow"
|
||||
android:value="true" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -12,18 +12,19 @@ import android.os.IBinder;
|
|||
import com.termux.R;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.data.IntentUtils;
|
||||
import com.termux.shared.file.TermuxFileUtils;
|
||||
import com.termux.shared.termux.plugins.TermuxPluginUtils;
|
||||
import com.termux.shared.termux.file.TermuxFileUtils;
|
||||
import com.termux.shared.file.filesystem.FileType;
|
||||
import com.termux.shared.models.errors.Errno;
|
||||
import com.termux.shared.models.errors.Error;
|
||||
import com.termux.shared.errors.Errno;
|
||||
import com.termux.shared.errors.Error;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.notification.NotificationUtils;
|
||||
import com.termux.app.utils.PluginUtils;
|
||||
import com.termux.shared.models.ExecutionCommand;
|
||||
import com.termux.shared.shell.command.ExecutionCommand;
|
||||
import com.termux.shared.shell.command.ExecutionCommand.Runner;
|
||||
|
||||
/**
|
||||
* A service that receives {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent from third party apps and
|
||||
|
|
@ -62,6 +63,8 @@ public class RunCommandService extends Service {
|
|||
// Run again in case service is already started and onCreate() is not called
|
||||
runStartForeground();
|
||||
|
||||
Logger.logVerboseExtended(LOG_TAG, "Intent Received:\n" + IntentUtils.getIntentString(intent));
|
||||
|
||||
ExecutionCommand executionCommand = new ExecutionCommand();
|
||||
executionCommand.pluginAPIHelp = this.getString(R.string.error_run_command_service_api_help, RUN_COMMAND_SERVICE.RUN_COMMAND_API_HELP_URL);
|
||||
|
||||
|
|
@ -72,7 +75,7 @@ public class RunCommandService extends Service {
|
|||
if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) {
|
||||
errmsg = this.getString(R.string.error_run_command_service_invalid_intent_action, intent.getAction());
|
||||
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return stopService();
|
||||
}
|
||||
|
||||
|
|
@ -100,9 +103,21 @@ public class RunCommandService extends Service {
|
|||
|
||||
executionCommand.stdin = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_STDIN, null);
|
||||
executionCommand.workingDirectory = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_WORKDIR, null);
|
||||
executionCommand.inBackground = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false);
|
||||
|
||||
// If EXTRA_RUNNER is passed, use that, otherwise check EXTRA_BACKGROUND and default to Runner.TERMINAL_SESSION
|
||||
executionCommand.runner = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RUNNER,
|
||||
(intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false) ? Runner.APP_SHELL.getName() : Runner.TERMINAL_SESSION.getName()));
|
||||
if (Runner.runnerOf(executionCommand.runner) == null) {
|
||||
errmsg = this.getString(R.string.error_run_command_service_invalid_execution_command_runner, executionCommand.runner);
|
||||
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return stopService();
|
||||
}
|
||||
|
||||
executionCommand.backgroundCustomLogLevel = IntentUtils.getIntegerExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL, null);
|
||||
executionCommand.sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION);
|
||||
executionCommand.shellName = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_SHELL_NAME, null);
|
||||
executionCommand.shellCreateMode = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_SHELL_CREATE_MODE, null);
|
||||
executionCommand.commandLabel = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL, "RUN_COMMAND Execution Intent Command");
|
||||
executionCommand.commandDescription = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION, null);
|
||||
executionCommand.commandHelp = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_HELP, null);
|
||||
|
|
@ -122,10 +137,10 @@ public class RunCommandService extends Service {
|
|||
// 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.checkIfAllowExternalAppsPolicyIsViolated(this, LOG_TAG);
|
||||
errmsg = TermuxPluginUtils.checkIfAllowExternalAppsPolicyIsViolated(this, LOG_TAG);
|
||||
if (errmsg != null) {
|
||||
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true);
|
||||
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true);
|
||||
return stopService();
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +150,7 @@ public class RunCommandService extends Service {
|
|||
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(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return stopService();
|
||||
}
|
||||
|
||||
|
|
@ -149,7 +164,7 @@ public class RunCommandService extends Service {
|
|||
false);
|
||||
if (error != null) {
|
||||
executionCommand.setStateFailed(error);
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return stopService();
|
||||
}
|
||||
|
||||
|
|
@ -170,7 +185,7 @@ public class RunCommandService extends Service {
|
|||
false, true);
|
||||
if (error != null) {
|
||||
executionCommand.setStateFailed(error);
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return stopService();
|
||||
}
|
||||
}
|
||||
|
|
@ -195,9 +210,11 @@ public class RunCommandService extends Service {
|
|||
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_RUNNER, executionCommand.runner);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL, DataUtils.getStringFromInteger(executionCommand.backgroundCustomLogLevel, null));
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION, executionCommand.sessionAction);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_SHELL_NAME, executionCommand.shellName);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_SHELL_CREATE_MODE, executionCommand.shellCreateMode);
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
package com.termux.app;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.BroadcastReceiver;
|
||||
|
|
@ -11,10 +9,7 @@ import android.content.Context;
|
|||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.view.ContextMenu;
|
||||
|
|
@ -25,7 +20,6 @@ import android.view.MenuItem;
|
|||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.view.autofill.AutofillManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ListView;
|
||||
|
|
@ -33,36 +27,46 @@ import android.widget.RelativeLayout;
|
|||
import android.widget.Toast;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.api.file.FileReceiverActivity;
|
||||
import com.termux.app.terminal.TermuxActivityRootView;
|
||||
import com.termux.app.terminal.TermuxTerminalSessionActivityClient;
|
||||
import com.termux.app.terminal.io.TermuxTerminalExtraKeys;
|
||||
import com.termux.shared.activities.ReportActivity;
|
||||
import com.termux.shared.packages.PermissionUtils;
|
||||
import com.termux.shared.activity.ActivityUtils;
|
||||
import com.termux.shared.activity.media.AppCompatActivityUtils;
|
||||
import com.termux.shared.data.IntentUtils;
|
||||
import com.termux.shared.android.PermissionUtils;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
|
||||
import com.termux.app.activities.HelpActivity;
|
||||
import com.termux.app.activities.SettingsActivity;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.termux.crash.TermuxCrashUtils;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.app.terminal.TermuxSessionsListViewController;
|
||||
import com.termux.app.terminal.io.TerminalToolbarViewPager;
|
||||
import com.termux.app.terminal.TermuxTerminalSessionClient;
|
||||
import com.termux.app.terminal.TermuxTerminalViewClient;
|
||||
import com.termux.shared.terminal.io.extrakeys.ExtraKeysView;
|
||||
import com.termux.app.settings.properties.TermuxAppSharedProperties;
|
||||
import com.termux.shared.interact.TextInputDialogUtils;
|
||||
import com.termux.shared.termux.extrakeys.ExtraKeysView;
|
||||
import com.termux.shared.termux.interact.TextInputDialogUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
import com.termux.shared.termux.settings.properties.TermuxAppSharedProperties;
|
||||
import com.termux.shared.termux.theme.TermuxThemeUtils;
|
||||
import com.termux.shared.theme.NightMode;
|
||||
import com.termux.shared.view.ViewUtils;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
import com.termux.terminal.TerminalSessionClient;
|
||||
import com.termux.app.utils.CrashUtils;
|
||||
import com.termux.view.TerminalView;
|
||||
import com.termux.view.TerminalViewClient;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* A terminal emulator activity.
|
||||
* <p/>
|
||||
|
|
@ -73,7 +77,7 @@ import androidx.viewpager.widget.ViewPager;
|
|||
* </ul>
|
||||
* about memory leaks.
|
||||
*/
|
||||
public final class TermuxActivity extends Activity implements ServiceConnection {
|
||||
public final class TermuxActivity extends AppCompatActivity implements ServiceConnection {
|
||||
|
||||
/**
|
||||
* The connection to the {@link TermuxService}. Requested in {@link #onCreate(Bundle)} with a call to
|
||||
|
|
@ -97,7 +101,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
* The {@link TerminalSessionClient} interface implementation to allow for communication between
|
||||
* {@link TerminalSession} and {@link TermuxActivity}.
|
||||
*/
|
||||
TermuxTerminalSessionClient mTermuxTerminalSessionClient;
|
||||
TermuxTerminalSessionActivityClient mTermuxTerminalSessionActivityClient;
|
||||
|
||||
/**
|
||||
* Termux app shared preferences manager.
|
||||
|
|
@ -105,7 +109,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
private TermuxAppSharedPreferences mPreferences;
|
||||
|
||||
/**
|
||||
* Termux app shared properties manager, loaded from termux.properties
|
||||
* Termux app SharedProperties loaded from termux.properties
|
||||
*/
|
||||
private TermuxAppSharedProperties mProperties;
|
||||
|
||||
|
|
@ -124,6 +128,11 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
*/
|
||||
ExtraKeysView mExtraKeysView;
|
||||
|
||||
/**
|
||||
* The client for the {@link #mExtraKeysView}.
|
||||
*/
|
||||
TermuxTerminalExtraKeys mTermuxTerminalExtraKeys;
|
||||
|
||||
/**
|
||||
* The termux sessions list controller.
|
||||
*/
|
||||
|
|
@ -148,7 +157,14 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
/**
|
||||
* If onResume() was called after onCreate().
|
||||
*/
|
||||
private boolean isOnResumeAfterOnCreate = false;
|
||||
private boolean mIsOnResumeAfterOnCreate = false;
|
||||
|
||||
/**
|
||||
* If activity was restarted like due to call to {@link #recreate()} after receiving
|
||||
* {@link TERMUX_ACTIVITY#ACTION_RELOAD_STYLE}, system dark night mode was changed or activity
|
||||
* was killed by android.
|
||||
*/
|
||||
private boolean mIsActivityRecreated = false;
|
||||
|
||||
/**
|
||||
* The {@link TermuxActivity} is in an invalid state and must not be run.
|
||||
|
|
@ -157,12 +173,14 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
|
||||
private int mNavBarHeight;
|
||||
|
||||
private int mTerminalToolbarDefaultHeight;
|
||||
private float mTerminalToolbarDefaultHeight;
|
||||
|
||||
|
||||
private static final int CONTEXT_MENU_SELECT_URL_ID = 0;
|
||||
private static final int CONTEXT_MENU_SHARE_TRANSCRIPT_ID = 1;
|
||||
private static final int CONTEXT_MENU_AUTOFILL_ID = 2;
|
||||
private static final int CONTEXT_MENU_SHARE_SELECTED_TEXT = 10;
|
||||
private static final int CONTEXT_MENU_AUTOFILL_USERNAME = 11;
|
||||
private static final int CONTEXT_MENU_AUTOFILL_PASSWORD = 2;
|
||||
private static final int CONTEXT_MENU_RESET_TERMINAL_ID = 3;
|
||||
private static final int CONTEXT_MENU_KILL_PROCESS_ID = 4;
|
||||
private static final int CONTEXT_MENU_STYLING_ID = 5;
|
||||
|
|
@ -172,24 +190,24 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
private static final int CONTEXT_MENU_REPORT_ID = 9;
|
||||
|
||||
private static final String ARG_TERMINAL_TOOLBAR_TEXT_INPUT = "terminal_toolbar_text_input";
|
||||
private static final String ARG_ACTIVITY_RECREATED = "activity_recreated";
|
||||
|
||||
private static final String LOG_TAG = "TermuxActivity";
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
|
||||
Logger.logDebug(LOG_TAG, "onCreate");
|
||||
isOnResumeAfterOnCreate = true;
|
||||
mIsOnResumeAfterOnCreate = true;
|
||||
|
||||
// Check if a crash happened on last run of the app and show a
|
||||
// notification with the crash details if it did
|
||||
CrashUtils.notifyAppCrashOnLastRun(this, LOG_TAG);
|
||||
if (savedInstanceState != null)
|
||||
mIsActivityRecreated = savedInstanceState.getBoolean(ARG_ACTIVITY_RECREATED, false);
|
||||
|
||||
// Delete ReportInfo serialized object files from cache older than 14 days
|
||||
ReportActivity.deleteReportInfoFilesOlderThanXDays(this, 14, false);
|
||||
|
||||
// Load termux shared properties
|
||||
mProperties = new TermuxAppSharedProperties(this);
|
||||
// Load Termux app SharedProperties from disk
|
||||
mProperties = TermuxAppSharedProperties.getProperties();
|
||||
reloadProperties();
|
||||
|
||||
setActivityTheme();
|
||||
|
||||
|
|
@ -223,8 +241,6 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
}
|
||||
|
||||
setDrawerTheme();
|
||||
|
||||
setTermuxTerminalViewAndClients();
|
||||
|
||||
setTerminalToolbarView(savedInstanceState);
|
||||
|
|
@ -237,14 +253,26 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
|
||||
registerForContextMenu(mTerminalView);
|
||||
|
||||
// Start the {@link TermuxService} and make it run regardless of who is bound to it
|
||||
Intent serviceIntent = new Intent(this, TermuxService.class);
|
||||
startService(serviceIntent);
|
||||
FileReceiverActivity.updateFileReceiverActivityComponentsState(this);
|
||||
|
||||
// Attempt to bind to the service, this will call the {@link #onServiceConnected(ComponentName, IBinder)}
|
||||
// callback if it succeeds.
|
||||
if (!bindService(serviceIntent, this, 0))
|
||||
throw new RuntimeException("bindService() failed");
|
||||
try {
|
||||
// Start the {@link TermuxService} and make it run regardless of who is bound to it
|
||||
Intent serviceIntent = new Intent(this, TermuxService.class);
|
||||
startService(serviceIntent);
|
||||
|
||||
// Attempt to bind to the service, this will call the {@link #onServiceConnected(ComponentName, IBinder)}
|
||||
// callback if it succeeds.
|
||||
if (!bindService(serviceIntent, this, 0))
|
||||
throw new RuntimeException("bindService() failed");
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG,"TermuxActivity failed to start TermuxService", e);
|
||||
Logger.showToast(this,
|
||||
getString(e.getMessage() != null && e.getMessage().contains("app is in background") ?
|
||||
R.string.error_termux_service_start_failed_bg : R.string.error_termux_service_start_failed_general),
|
||||
true);
|
||||
mIsInvalidState = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Send the {@link TermuxConstants#BROADCAST_TERMUX_OPENED} broadcast to notify apps that Termux
|
||||
// app has been opened.
|
||||
|
|
@ -261,8 +289,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
|
||||
mIsVisible = true;
|
||||
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.onStart();
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.onStart();
|
||||
|
||||
if (mTermuxTerminalViewClient != null)
|
||||
mTermuxTerminalViewClient.onStart();
|
||||
|
|
@ -281,13 +309,17 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
|
||||
if (mIsInvalidState) return;
|
||||
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.onResume();
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.onResume();
|
||||
|
||||
if (mTermuxTerminalViewClient != null)
|
||||
mTermuxTerminalViewClient.onResume();
|
||||
|
||||
isOnResumeAfterOnCreate = false;
|
||||
// Check if a crash happened on last run of the app or if a plugin crashed and show a
|
||||
// notification with the crash details if it did
|
||||
TermuxCrashUtils.notifyAppCrashFromCrashLogFile(this, LOG_TAG);
|
||||
|
||||
mIsOnResumeAfterOnCreate = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -300,15 +332,15 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
|
||||
mIsVisible = false;
|
||||
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.onStop();
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.onStop();
|
||||
|
||||
if (mTermuxTerminalViewClient != null)
|
||||
mTermuxTerminalViewClient.onStop();
|
||||
|
||||
removeTermuxActivityRootViewGlobalLayoutListener();
|
||||
|
||||
unregisterTermuxActivityBroadcastReceiever();
|
||||
unregisterTermuxActivityBroadcastReceiver();
|
||||
getDrawer().closeDrawers();
|
||||
}
|
||||
|
||||
|
|
@ -335,8 +367,11 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
|
||||
Logger.logVerbose(LOG_TAG, "onSaveInstanceState");
|
||||
|
||||
super.onSaveInstanceState(savedInstanceState);
|
||||
saveTerminalToolbarTextInput(savedInstanceState);
|
||||
savedInstanceState.putBoolean(ARG_ACTIVITY_RECREATED, true);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -350,24 +385,25 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
*/
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName componentName, IBinder service) {
|
||||
|
||||
Logger.logDebug(LOG_TAG, "onServiceConnected");
|
||||
|
||||
mTermuxService = ((TermuxService.LocalBinder) service).service;
|
||||
|
||||
setTermuxSessionsListView();
|
||||
|
||||
final Intent intent = getIntent();
|
||||
setIntent(null);
|
||||
|
||||
if (mTermuxService.isTermuxSessionsEmpty()) {
|
||||
if (mIsVisible) {
|
||||
TermuxInstaller.setupBootstrapIfNeeded(TermuxActivity.this, () -> {
|
||||
if (mTermuxService == null) return; // Activity might have been destroyed.
|
||||
try {
|
||||
Bundle bundle = getIntent().getExtras();
|
||||
boolean launchFailsafe = false;
|
||||
if (bundle != null) {
|
||||
launchFailsafe = bundle.getBoolean(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false);
|
||||
if (intent != null && intent.getExtras() != null) {
|
||||
launchFailsafe = intent.getExtras().getBoolean(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false);
|
||||
}
|
||||
mTermuxTerminalSessionClient.addNewSession(launchFailsafe, null);
|
||||
mTermuxTerminalSessionActivityClient.addNewSession(launchFailsafe, null);
|
||||
} catch (WindowManager.BadTokenException e) {
|
||||
// Activity finished - ignore.
|
||||
}
|
||||
|
|
@ -377,23 +413,24 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
finishActivityIfNotFinishing();
|
||||
}
|
||||
} else {
|
||||
Intent i = getIntent();
|
||||
if (i != null && Intent.ACTION_RUN.equals(i.getAction())) {
|
||||
// If termux was started from launcher "New session" shortcut and activity is recreated,
|
||||
// then the original intent will be re-delivered, resulting in a new session being re-added
|
||||
// each time.
|
||||
if (!mIsActivityRecreated && intent != null && Intent.ACTION_RUN.equals(intent.getAction())) {
|
||||
// Android 7.1 app shortcut from res/xml/shortcuts.xml.
|
||||
boolean isFailSafe = i.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false);
|
||||
mTermuxTerminalSessionClient.addNewSession(isFailSafe, null);
|
||||
boolean isFailSafe = intent.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false);
|
||||
mTermuxTerminalSessionActivityClient.addNewSession(isFailSafe, null);
|
||||
} else {
|
||||
mTermuxTerminalSessionClient.setCurrentSession(mTermuxTerminalSessionClient.getCurrentStoredSessionOrLast());
|
||||
mTermuxTerminalSessionActivityClient.setCurrentSession(mTermuxTerminalSessionActivityClient.getCurrentStoredSessionOrLast());
|
||||
}
|
||||
}
|
||||
|
||||
// Update the {@link TerminalSession} and {@link TerminalEmulator} clients.
|
||||
mTermuxService.setTermuxTerminalSessionClient(mTermuxTerminalSessionClient);
|
||||
mTermuxService.setTermuxTerminalSessionClient(mTermuxTerminalSessionActivityClient);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
|
||||
Logger.logDebug(LOG_TAG, "onServiceDisconnected");
|
||||
|
||||
// Respect being stopped from the {@link TermuxService} notification action.
|
||||
|
|
@ -404,20 +441,24 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
|
||||
|
||||
|
||||
private void setActivityTheme() {
|
||||
if (mProperties.isUsingBlackUI()) {
|
||||
this.setTheme(R.style.Theme_Termux_Black);
|
||||
} else {
|
||||
this.setTheme(R.style.Theme_Termux);
|
||||
}
|
||||
|
||||
private void reloadProperties() {
|
||||
mProperties.loadTermuxPropertiesFromDisk();
|
||||
|
||||
if (mTermuxTerminalViewClient != null)
|
||||
mTermuxTerminalViewClient.onReloadProperties();
|
||||
}
|
||||
|
||||
private void setDrawerTheme() {
|
||||
if (mProperties.isUsingBlackUI()) {
|
||||
findViewById(R.id.left_drawer).setBackgroundColor(ContextCompat.getColor(this,
|
||||
android.R.color.background_dark));
|
||||
((ImageButton) findViewById(R.id.settings_button)).setColorFilter(Color.WHITE);
|
||||
}
|
||||
|
||||
|
||||
private void setActivityTheme() {
|
||||
// Update NightMode.APP_NIGHT_MODE
|
||||
TermuxThemeUtils.setAppNightMode(mProperties.getNightMode());
|
||||
|
||||
// Set activity night mode. If NightMode.SYSTEM is set, then android will automatically
|
||||
// trigger recreation of activity when uiMode/dark mode configuration is changed so that
|
||||
// day or night theme takes affect.
|
||||
AppCompatActivityUtils.setNightMode(this, NightMode.getAppNightMode().getName(), true);
|
||||
}
|
||||
|
||||
private void setMargins() {
|
||||
|
|
@ -442,8 +483,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
|
||||
private void setTermuxTerminalViewAndClients() {
|
||||
// Set termux terminal view and session clients
|
||||
mTermuxTerminalSessionClient = new TermuxTerminalSessionClient(this);
|
||||
mTermuxTerminalViewClient = new TermuxTerminalViewClient(this, mTermuxTerminalSessionClient);
|
||||
mTermuxTerminalSessionActivityClient = new TermuxTerminalSessionActivityClient(this);
|
||||
mTermuxTerminalViewClient = new TermuxTerminalViewClient(this, mTermuxTerminalSessionActivityClient);
|
||||
|
||||
// Set termux terminal view
|
||||
mTerminalView = findViewById(R.id.terminal_view);
|
||||
|
|
@ -452,8 +493,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
if (mTermuxTerminalViewClient != null)
|
||||
mTermuxTerminalViewClient.onCreate();
|
||||
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.onCreate();
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.onCreate();
|
||||
}
|
||||
|
||||
private void setTermuxSessionsListView() {
|
||||
|
|
@ -467,6 +508,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
|
||||
|
||||
private void setTerminalToolbarView(Bundle savedInstanceState) {
|
||||
mTermuxTerminalExtraKeys = new TermuxTerminalExtraKeys(this, mTerminalView,
|
||||
mTermuxTerminalViewClient, mTermuxTerminalSessionActivityClient);
|
||||
|
||||
final ViewPager terminalToolbarViewPager = getTerminalToolbarViewPager();
|
||||
if (mPreferences.shouldShowTerminalToolbar()) terminalToolbarViewPager.setVisibility(View.VISIBLE);
|
||||
|
||||
|
|
@ -488,8 +532,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
if (terminalToolbarViewPager == null) return;
|
||||
|
||||
ViewGroup.LayoutParams layoutParams = terminalToolbarViewPager.getLayoutParams();
|
||||
layoutParams.height = (int) Math.round(mTerminalToolbarDefaultHeight *
|
||||
(mProperties.getExtraKeysInfo() == null ? 0 : mProperties.getExtraKeysInfo().getMatrix().length) *
|
||||
layoutParams.height = Math.round(mTerminalToolbarDefaultHeight *
|
||||
(mTermuxTerminalExtraKeys.getExtraKeysInfo() == null ? 0 : mTermuxTerminalExtraKeys.getExtraKeysInfo().getMatrix().length) *
|
||||
mProperties.getTerminalToolbarHeightScaleFactor());
|
||||
terminalToolbarViewPager.setLayoutParams(layoutParams);
|
||||
}
|
||||
|
|
@ -510,7 +554,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
private void saveTerminalToolbarTextInput(Bundle savedInstanceState) {
|
||||
if (savedInstanceState == null) return;
|
||||
|
||||
final EditText textInputView = findViewById(R.id.terminal_toolbar_text_input);
|
||||
final EditText textInputView = findViewById(R.id.terminal_toolbar_text_input);
|
||||
if (textInputView != null) {
|
||||
String textInput = textInputView.getText().toString();
|
||||
if (!textInput.isEmpty()) savedInstanceState.putString(ARG_TERMINAL_TOOLBAR_TEXT_INPUT, textInput);
|
||||
|
|
@ -522,17 +566,17 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
private void setSettingsButtonView() {
|
||||
ImageButton settingsButton = findViewById(R.id.settings_button);
|
||||
settingsButton.setOnClickListener(v -> {
|
||||
startActivity(new Intent(this, SettingsActivity.class));
|
||||
ActivityUtils.startActivity(this, new Intent(this, SettingsActivity.class));
|
||||
});
|
||||
}
|
||||
|
||||
private void setNewSessionButtonView() {
|
||||
View newSessionButton = findViewById(R.id.new_session_button);
|
||||
newSessionButton.setOnClickListener(v -> mTermuxTerminalSessionClient.addNewSession(false, null));
|
||||
newSessionButton.setOnClickListener(v -> mTermuxTerminalSessionActivityClient.addNewSession(false, null));
|
||||
newSessionButton.setOnLongClickListener(v -> {
|
||||
TextInputDialogUtils.textInput(TermuxActivity.this, R.string.title_create_named_session, null,
|
||||
R.string.action_create_named_session_confirm, text -> mTermuxTerminalSessionClient.addNewSession(false, text),
|
||||
R.string.action_new_session_failsafe, text -> mTermuxTerminalSessionClient.addNewSession(true, text),
|
||||
R.string.action_create_named_session_confirm, text -> mTermuxTerminalSessionActivityClient.addNewSession(false, text),
|
||||
R.string.action_new_session_failsafe, text -> mTermuxTerminalSessionActivityClient.addNewSession(true, text),
|
||||
-1, null, null);
|
||||
return true;
|
||||
});
|
||||
|
|
@ -587,17 +631,16 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
TerminalSession currentSession = getCurrentSession();
|
||||
if (currentSession == null) return;
|
||||
|
||||
boolean addAutoFillMenu = false;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
AutofillManager autofillManager = getSystemService(AutofillManager.class);
|
||||
if (autofillManager != null && autofillManager.isEnabled()) {
|
||||
addAutoFillMenu = true;
|
||||
}
|
||||
}
|
||||
boolean autoFillEnabled = mTerminalView.isAutoFillEnabled();
|
||||
|
||||
menu.add(Menu.NONE, CONTEXT_MENU_SELECT_URL_ID, Menu.NONE, R.string.action_select_url);
|
||||
menu.add(Menu.NONE, CONTEXT_MENU_SHARE_TRANSCRIPT_ID, Menu.NONE, R.string.action_share_transcript);
|
||||
if (addAutoFillMenu) menu.add(Menu.NONE, CONTEXT_MENU_AUTOFILL_ID, Menu.NONE, R.string.action_autofill_password);
|
||||
if (!DataUtils.isNullOrEmpty(mTerminalView.getStoredSelectedText()))
|
||||
menu.add(Menu.NONE, CONTEXT_MENU_SHARE_SELECTED_TEXT, Menu.NONE, R.string.action_share_selected_text);
|
||||
if (autoFillEnabled)
|
||||
menu.add(Menu.NONE, CONTEXT_MENU_AUTOFILL_USERNAME, Menu.NONE, R.string.action_autofill_username);
|
||||
if (autoFillEnabled)
|
||||
menu.add(Menu.NONE, CONTEXT_MENU_AUTOFILL_PASSWORD, Menu.NONE, R.string.action_autofill_password);
|
||||
menu.add(Menu.NONE, CONTEXT_MENU_RESET_TERMINAL_ID, Menu.NONE, R.string.action_reset_terminal);
|
||||
menu.add(Menu.NONE, CONTEXT_MENU_KILL_PROCESS_ID, Menu.NONE, getResources().getString(R.string.action_kill_process, getCurrentSession().getPid())).setEnabled(currentSession.isRunning());
|
||||
menu.add(Menu.NONE, CONTEXT_MENU_STYLING_ID, Menu.NONE, R.string.action_style_terminal);
|
||||
|
|
@ -625,8 +668,14 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
case CONTEXT_MENU_SHARE_TRANSCRIPT_ID:
|
||||
mTermuxTerminalViewClient.shareSessionTranscript();
|
||||
return true;
|
||||
case CONTEXT_MENU_AUTOFILL_ID:
|
||||
requestAutoFill();
|
||||
case CONTEXT_MENU_SHARE_SELECTED_TEXT:
|
||||
mTermuxTerminalViewClient.shareSelectedText();
|
||||
return true;
|
||||
case CONTEXT_MENU_AUTOFILL_USERNAME:
|
||||
mTerminalView.requestAutoFillUsername();
|
||||
return true;
|
||||
case CONTEXT_MENU_AUTOFILL_PASSWORD:
|
||||
mTerminalView.requestAutoFillPassword();
|
||||
return true;
|
||||
case CONTEXT_MENU_RESET_TERMINAL_ID:
|
||||
onResetTerminalSession(session);
|
||||
|
|
@ -641,10 +690,10 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
toggleKeepScreenOn();
|
||||
return true;
|
||||
case CONTEXT_MENU_HELP_ID:
|
||||
startActivity(new Intent(this, HelpActivity.class));
|
||||
ActivityUtils.startActivity(this, new Intent(this, HelpActivity.class));
|
||||
return true;
|
||||
case CONTEXT_MENU_SETTINGS_ID:
|
||||
startActivity(new Intent(this, SettingsActivity.class));
|
||||
ActivityUtils.startActivity(this, new Intent(this, SettingsActivity.class));
|
||||
return true;
|
||||
case CONTEXT_MENU_REPORT_ID:
|
||||
mTermuxTerminalViewClient.reportIssueFromTranscript();
|
||||
|
|
@ -654,6 +703,13 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContextMenuClosed(Menu menu) {
|
||||
super.onContextMenuClosed(menu);
|
||||
// onContextMenuClosed() is triggered twice if back button is pressed to dismiss instead of tap for some reason
|
||||
mTerminalView.onContextMenuClosed(menu);
|
||||
}
|
||||
|
||||
private void showKillSessionDialog(TerminalSession session) {
|
||||
if (session == null) return;
|
||||
|
||||
|
|
@ -673,21 +729,23 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
session.reset();
|
||||
showToast(getResources().getString(R.string.msg_terminal_reset), true);
|
||||
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.onResetTerminalSession();
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.onResetTerminalSession();
|
||||
}
|
||||
}
|
||||
|
||||
private void showStylingDialog() {
|
||||
Intent stylingIntent = new Intent();
|
||||
stylingIntent.setClassName(TermuxConstants.TERMUX_STYLING_PACKAGE_NAME, TermuxConstants.TERMUX_STYLING.TERMUX_STYLING_ACTIVITY_NAME);
|
||||
stylingIntent.setClassName(TermuxConstants.TERMUX_STYLING_PACKAGE_NAME, TermuxConstants.TERMUX_STYLING_APP.TERMUX_STYLING_ACTIVITY_NAME);
|
||||
try {
|
||||
startActivity(stylingIntent);
|
||||
} catch (ActivityNotFoundException | IllegalArgumentException e) {
|
||||
// The startActivity() call is not documented to throw IllegalArgumentException.
|
||||
// However, crash reporting shows that it sometimes does, so catch it here.
|
||||
new AlertDialog.Builder(this).setMessage(getString(R.string.error_styling_not_installed))
|
||||
.setPositiveButton(R.string.action_styling_install, (dialog, which) -> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(TermuxConstants.TERMUX_STYLING_FDROID_PACKAGE_URL)))).setNegativeButton(android.R.string.cancel, null).show();
|
||||
.setPositiveButton(R.string.action_styling_install,
|
||||
(dialog, which) -> ActivityUtils.startActivity(this, new Intent(Intent.ACTION_VIEW, Uri.parse(TermuxConstants.TERMUX_STYLING_FDROID_PACKAGE_URL))))
|
||||
.setNegativeButton(android.R.string.cancel, null).show();
|
||||
}
|
||||
}
|
||||
private void toggleKeepScreenOn() {
|
||||
|
|
@ -700,37 +758,52 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
}
|
||||
}
|
||||
|
||||
private void requestAutoFill() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
AutofillManager autofillManager = getSystemService(AutofillManager.class);
|
||||
if (autofillManager != null && autofillManager.isEnabled()) {
|
||||
autofillManager.requestAutofill(mTerminalView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* For processes to access shared internal storage (/sdcard) we need this permission.
|
||||
* For processes to access primary external storage (/sdcard, /storage/emulated/0, ~/storage/shared),
|
||||
* termux needs to be granted legacy WRITE_EXTERNAL_STORAGE or MANAGE_EXTERNAL_STORAGE permissions
|
||||
* if targeting targetSdkVersion 30 (android 11) and running on sdk 30 (android 11) and higher.
|
||||
*/
|
||||
public boolean ensureStoragePermissionGranted() {
|
||||
if (PermissionUtils.checkPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||
return true;
|
||||
} else {
|
||||
Logger.logInfo(LOG_TAG, "Storage permission not granted, requesting permission.");
|
||||
PermissionUtils.requestPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE, PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION);
|
||||
return false;
|
||||
public void requestStoragePermission(boolean isPermissionCallback) {
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Do not ask for permission again
|
||||
int requestCode = isPermissionCallback ? -1 : PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION;
|
||||
|
||||
// If permission is granted, then also setup storage symlinks.
|
||||
if(PermissionUtils.checkAndRequestLegacyOrManageExternalStoragePermission(
|
||||
TermuxActivity.this, requestCode, !isPermissionCallback)) {
|
||||
if (isPermissionCallback)
|
||||
Logger.logInfoAndShowToast(TermuxActivity.this, LOG_TAG,
|
||||
getString(com.termux.shared.R.string.msg_storage_permission_granted_on_request));
|
||||
|
||||
TermuxInstaller.setupStorageSymlinks(TermuxActivity.this);
|
||||
} else {
|
||||
if (isPermissionCallback)
|
||||
Logger.logInfoAndShowToast(TermuxActivity.this, LOG_TAG,
|
||||
getString(com.termux.shared.R.string.msg_storage_permission_not_granted_on_request));
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
Logger.logVerbose(LOG_TAG, "onActivityResult: requestCode: " + requestCode + ", resultCode: " + resultCode + ", data: " + IntentUtils.getIntentString(data));
|
||||
if (requestCode == PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION) {
|
||||
requestStoragePermission(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
if (requestCode == PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
Logger.logInfo(LOG_TAG, "Storage permission granted by user on request.");
|
||||
TermuxInstaller.setupStorageSymlinks(this);
|
||||
} else {
|
||||
Logger.logInfo(LOG_TAG, "Storage permission denied by user on request.");
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
Logger.logVerbose(LOG_TAG, "onRequestPermissionsResult: requestCode: " + requestCode + ", permissions: " + Arrays.toString(permissions) + ", grantResults: " + Arrays.toString(grantResults));
|
||||
if (requestCode == PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION) {
|
||||
requestStoragePermission(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -752,6 +825,10 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
return mExtraKeysView;
|
||||
}
|
||||
|
||||
public TermuxTerminalExtraKeys getTermuxTerminalExtraKeys() {
|
||||
return mTermuxTerminalExtraKeys;
|
||||
}
|
||||
|
||||
public void setExtraKeysView(ExtraKeysView extraKeysView) {
|
||||
mExtraKeysView = extraKeysView;
|
||||
}
|
||||
|
|
@ -765,6 +842,10 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
return (ViewPager) findViewById(R.id.terminal_toolbar_view_pager);
|
||||
}
|
||||
|
||||
public float getTerminalToolbarDefaultHeight() {
|
||||
return mTerminalToolbarDefaultHeight;
|
||||
}
|
||||
|
||||
public boolean isTerminalViewSelected() {
|
||||
return getTerminalToolbarViewPager().getCurrentItem() == 0;
|
||||
}
|
||||
|
|
@ -783,7 +864,11 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
}
|
||||
|
||||
public boolean isOnResumeAfterOnCreate() {
|
||||
return isOnResumeAfterOnCreate;
|
||||
return mIsOnResumeAfterOnCreate;
|
||||
}
|
||||
|
||||
public boolean isActivityRecreated() {
|
||||
return mIsActivityRecreated;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -800,8 +885,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
return mTermuxTerminalViewClient;
|
||||
}
|
||||
|
||||
public TermuxTerminalSessionClient getTermuxTerminalSessionClient() {
|
||||
return mTermuxTerminalSessionClient;
|
||||
public TermuxTerminalSessionActivityClient getTermuxTerminalSessionClient() {
|
||||
return mTermuxTerminalSessionActivityClient;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
|
@ -823,25 +908,27 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
|
||||
|
||||
|
||||
public static void updateTermuxActivityStyling(Context context) {
|
||||
public static void updateTermuxActivityStyling(Context context, boolean recreateActivity) {
|
||||
// Make sure that terminal styling is always applied.
|
||||
Intent stylingIntent = new Intent(TERMUX_ACTIVITY.ACTION_RELOAD_STYLE);
|
||||
stylingIntent.putExtra(TERMUX_ACTIVITY.EXTRA_RECREATE_ACTIVITY, recreateActivity);
|
||||
context.sendBroadcast(stylingIntent);
|
||||
}
|
||||
|
||||
private void registerTermuxActivityBroadcastReceiver() {
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(TERMUX_ACTIVITY.ACTION_REQUEST_PERMISSIONS);
|
||||
intentFilter.addAction(TERMUX_ACTIVITY.ACTION_NOTIFY_APP_CRASH);
|
||||
intentFilter.addAction(TERMUX_ACTIVITY.ACTION_RELOAD_STYLE);
|
||||
intentFilter.addAction(TERMUX_ACTIVITY.ACTION_REQUEST_PERMISSIONS);
|
||||
|
||||
registerReceiver(mTermuxActivityBroadcastReceiver, intentFilter);
|
||||
}
|
||||
|
||||
private void unregisterTermuxActivityBroadcastReceiever() {
|
||||
private void unregisterTermuxActivityBroadcastReceiver() {
|
||||
unregisterReceiver(mTermuxActivityBroadcastReceiver);
|
||||
}
|
||||
|
||||
private void fixTermuxActivityBroadcastReceieverIntent(Intent intent) {
|
||||
private void fixTermuxActivityBroadcastReceiverIntent(Intent intent) {
|
||||
if (intent == null) return;
|
||||
|
||||
String extraReloadStyle = intent.getStringExtra(TERMUX_ACTIVITY.EXTRA_RELOAD_STYLE);
|
||||
|
|
@ -857,17 +944,20 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
if (intent == null) return;
|
||||
|
||||
if (mIsVisible) {
|
||||
fixTermuxActivityBroadcastReceieverIntent(intent);
|
||||
fixTermuxActivityBroadcastReceiverIntent(intent);
|
||||
|
||||
switch (intent.getAction()) {
|
||||
case TERMUX_ACTIVITY.ACTION_REQUEST_PERMISSIONS:
|
||||
Logger.logDebug(LOG_TAG, "Received intent to request storage permissions");
|
||||
if (ensureStoragePermissionGranted())
|
||||
TermuxInstaller.setupStorageSymlinks(TermuxActivity.this);
|
||||
case TERMUX_ACTIVITY.ACTION_NOTIFY_APP_CRASH:
|
||||
Logger.logDebug(LOG_TAG, "Received intent to notify app crash");
|
||||
TermuxCrashUtils.notifyAppCrashFromCrashLogFile(context, LOG_TAG);
|
||||
return;
|
||||
case TERMUX_ACTIVITY.ACTION_RELOAD_STYLE:
|
||||
Logger.logDebug(LOG_TAG, "Received intent to reload styling");
|
||||
reloadActivityStyling();
|
||||
reloadActivityStyling(intent.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_RECREATE_ACTIVITY, true));
|
||||
return;
|
||||
case TERMUX_ACTIVITY.ACTION_REQUEST_PERMISSIONS:
|
||||
Logger.logDebug(LOG_TAG, "Received intent to request storage permissions");
|
||||
requestStoragePermission(false);
|
||||
return;
|
||||
default:
|
||||
}
|
||||
|
|
@ -875,41 +965,43 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
}
|
||||
}
|
||||
|
||||
private void reloadActivityStyling() {
|
||||
if (mProperties!= null) {
|
||||
mProperties.loadTermuxPropertiesFromDisk();
|
||||
private void reloadActivityStyling(boolean recreateActivity) {
|
||||
if (mProperties != null) {
|
||||
reloadProperties();
|
||||
|
||||
if (mExtraKeysView != null) {
|
||||
mExtraKeysView.setButtonTextAllCaps(mProperties.shouldExtraKeysTextBeAllCaps());
|
||||
mExtraKeysView.reload(mProperties.getExtraKeysInfo());
|
||||
mExtraKeysView.reload(mTermuxTerminalExtraKeys.getExtraKeysInfo(), mTerminalToolbarDefaultHeight);
|
||||
}
|
||||
|
||||
// Update NightMode.APP_NIGHT_MODE
|
||||
TermuxThemeUtils.setAppNightMode(mProperties.getNightMode());
|
||||
}
|
||||
|
||||
setMargins();
|
||||
setTerminalToolbarHeight();
|
||||
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.onReload();
|
||||
FileReceiverActivity.updateFileReceiverActivityComponentsState(this);
|
||||
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.onReloadActivityStyling();
|
||||
|
||||
if (mTermuxTerminalViewClient != null)
|
||||
mTermuxTerminalViewClient.onReload();
|
||||
|
||||
if (mTermuxService != null)
|
||||
mTermuxService.setTerminalTranscriptRows();
|
||||
mTermuxTerminalViewClient.onReloadActivityStyling();
|
||||
|
||||
// To change the activity and drawer theme, activity needs to be recreated.
|
||||
// But this will destroy the activity, and will call the onCreate() again.
|
||||
// We need to investigate if enabling this is wise, since all stored variables and
|
||||
// views will be destroyed and bindService() will be called again. Extra keys input
|
||||
// text will we restored since that has already been implemented. Terminal sessions
|
||||
// and transcripts are also already preserved. Theme does change properly too.
|
||||
// TermuxActivity.this.recreate();
|
||||
// It will destroy the activity, including all stored variables and views, and onCreate()
|
||||
// will be called again. Extra keys input text, terminal sessions and transcripts will be preserved.
|
||||
if (recreateActivity) {
|
||||
Logger.logDebug(LOG_TAG, "Recreating activity");
|
||||
TermuxActivity.this.recreate();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static void startTermuxActivity(@NonNull final Context context) {
|
||||
context.startActivity(newInstance(context));
|
||||
ActivityUtils.startActivity(context, newInstance(context));
|
||||
}
|
||||
|
||||
public static Intent newInstance(@NonNull final Context context) {
|
||||
|
|
|
|||
|
|
@ -1,29 +1,85 @@
|
|||
package com.termux.app;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
|
||||
import com.termux.shared.crash.TermuxCrashUtils;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.BuildConfig;
|
||||
import com.termux.shared.errors.Error;
|
||||
import com.termux.shared.logger.Logger;
|
||||
|
||||
import com.termux.shared.termux.TermuxBootstrap;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.crash.TermuxCrashUtils;
|
||||
import com.termux.shared.termux.file.TermuxFileUtils;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.termux.settings.properties.TermuxAppSharedProperties;
|
||||
import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment;
|
||||
import com.termux.shared.termux.shell.am.TermuxAmSocketServer;
|
||||
import com.termux.shared.termux.shell.TermuxShellManager;
|
||||
import com.termux.shared.termux.theme.TermuxThemeUtils;
|
||||
|
||||
public class TermuxApplication extends Application {
|
||||
|
||||
private static final String LOG_TAG = "TermuxApplication";
|
||||
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
// Set crash handler for the app
|
||||
TermuxCrashUtils.setCrashHandler(this);
|
||||
Context context = getApplicationContext();
|
||||
|
||||
// Set log level for the app
|
||||
setLogLevel();
|
||||
// Set crash handler for the app
|
||||
TermuxCrashUtils.setDefaultCrashHandler(this);
|
||||
|
||||
// Set log config for the app
|
||||
setLogConfig(context);
|
||||
|
||||
Logger.logDebug("Starting Application");
|
||||
|
||||
// Set TermuxBootstrap.TERMUX_APP_PACKAGE_MANAGER and TermuxBootstrap.TERMUX_APP_PACKAGE_VARIANT
|
||||
TermuxBootstrap.setTermuxPackageManagerAndVariant(BuildConfig.TERMUX_PACKAGE_VARIANT);
|
||||
|
||||
// Init app wide SharedProperties loaded from termux.properties
|
||||
TermuxAppSharedProperties properties = TermuxAppSharedProperties.init(context);
|
||||
|
||||
// Init app wide shell manager
|
||||
TermuxShellManager shellManager = TermuxShellManager.init(context);
|
||||
|
||||
// Set NightMode.APP_NIGHT_MODE
|
||||
TermuxThemeUtils.setAppNightMode(properties.getNightMode());
|
||||
|
||||
// Check and create termux files directory. If failed to access it like in case of secondary
|
||||
// user or external sd card installation, then don't run files directory related code
|
||||
Error error = TermuxFileUtils.isTermuxFilesDirectoryAccessible(this, true, true);
|
||||
boolean isTermuxFilesDirectoryAccessible = error == null;
|
||||
if (isTermuxFilesDirectoryAccessible) {
|
||||
Logger.logInfo(LOG_TAG, "Termux files directory is accessible");
|
||||
|
||||
error = TermuxFileUtils.isAppsTermuxAppDirectoryAccessible(true, true);
|
||||
if (error != null) {
|
||||
Logger.logErrorExtended(LOG_TAG, "Create apps/termux-app directory failed\n" + error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup termux-am-socket server
|
||||
TermuxAmSocketServer.setupTermuxAmSocketServer(context);
|
||||
} else {
|
||||
Logger.logErrorExtended(LOG_TAG, "Termux files directory is not accessible\n" + error);
|
||||
}
|
||||
|
||||
// Init TermuxShellEnvironment constants and caches after everything has been setup including termux-am-socket server
|
||||
TermuxShellEnvironment.init(this);
|
||||
|
||||
if (isTermuxFilesDirectoryAccessible) {
|
||||
TermuxShellEnvironment.writeEnvironmentToFile(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void setLogLevel() {
|
||||
public static void setLogConfig(Context context) {
|
||||
Logger.setDefaultLogTag(TermuxConstants.TERMUX_APP_NAME);
|
||||
|
||||
// Load the log level from shared preferences and set it to the {@link Logger.CURRENT_LOG_LEVEL}
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(getApplicationContext());
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
||||
if (preferences == null) return;
|
||||
preferences.setLogLevel(null, preferences.getLogLevel());
|
||||
Logger.logDebug("Starting Application");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,22 +4,24 @@ import android.app.Activity;
|
|||
import android.app.AlertDialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.system.Os;
|
||||
import android.util.Pair;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.utils.CrashUtils;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.file.TermuxFileUtils;
|
||||
import com.termux.shared.termux.crash.TermuxCrashUtils;
|
||||
import com.termux.shared.termux.file.TermuxFileUtils;
|
||||
import com.termux.shared.interact.MessageDialogUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.models.errors.Error;
|
||||
import com.termux.shared.packages.PackageUtils;
|
||||
import com.termux.shared.errors.Error;
|
||||
import com.termux.shared.android.PackageUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
|
|
@ -71,8 +73,9 @@ final class TermuxInstaller {
|
|||
|
||||
// Termux can only be run as the primary user (device owner) since only that
|
||||
// account has the expected file system paths. Verify that:
|
||||
if (!PackageUtils.isCurrentUserThePrimaryUser(activity)) {
|
||||
bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message, MarkdownUtils.getMarkdownCodeForString(TERMUX_PREFIX_DIR_PATH, false));
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && !PackageUtils.isCurrentUserThePrimaryUser(activity)) {
|
||||
bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message,
|
||||
MarkdownUtils.getMarkdownCodeForString(TERMUX_PREFIX_DIR_PATH, false));
|
||||
Logger.logError(LOG_TAG, "isFilesDirectoryAccessible: " + isFilesDirectoryAccessible);
|
||||
Logger.logError(LOG_TAG, bootstrapErrorMessage);
|
||||
sendBootstrapCrashReportNotification(activity, bootstrapErrorMessage);
|
||||
|
|
@ -83,7 +86,14 @@ final class TermuxInstaller {
|
|||
}
|
||||
|
||||
if (!isFilesDirectoryAccessible) {
|
||||
bootstrapErrorMessage = Error.getMinimalErrorString(filesDirectoryAccessibleError) + "\nTERMUX_FILES_DIR: " + MarkdownUtils.getMarkdownCodeForString(TermuxConstants.TERMUX_FILES_DIR_PATH, false);
|
||||
bootstrapErrorMessage = Error.getMinimalErrorString(filesDirectoryAccessibleError);
|
||||
//noinspection SdCardPath
|
||||
if (PackageUtils.isAppInstalledOnExternalStorage(activity) &&
|
||||
!TermuxConstants.TERMUX_FILES_DIR_PATH.equals(activity.getFilesDir().getAbsolutePath().replaceAll("^/data/user/0/", "/data/data/"))) {
|
||||
bootstrapErrorMessage += "\n\n" + activity.getString(R.string.bootstrap_error_installed_on_portable_sd,
|
||||
MarkdownUtils.getMarkdownCodeForString(TERMUX_PREFIX_DIR_PATH, false));
|
||||
}
|
||||
|
||||
Logger.logError(LOG_TAG, bootstrapErrorMessage);
|
||||
sendBootstrapCrashReportNotification(activity, bootstrapErrorMessage);
|
||||
MessageDialogUtils.showMessage(activity,
|
||||
|
|
@ -94,10 +104,8 @@ final class TermuxInstaller {
|
|||
|
||||
// If prefix directory exists, even if its a symlink to a valid directory and symlink is not broken/dangling
|
||||
if (FileUtils.directoryFileExists(TERMUX_PREFIX_DIR_PATH, true)) {
|
||||
File[] PREFIX_FILE_LIST = TERMUX_PREFIX_DIR.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 termux prefix directory \"" + TERMUX_PREFIX_DIR_PATH + "\" exists but is empty or only contains the tmp directory.");
|
||||
if (TermuxFileUtils.isTermuxPrefixDirectoryEmpty()) {
|
||||
Logger.logInfo(LOG_TAG, "The termux prefix directory \"" + TERMUX_PREFIX_DIR_PATH + "\" exists but is empty or only contains specific unimportant files.");
|
||||
} else {
|
||||
whenDone.run();
|
||||
return;
|
||||
|
|
@ -209,6 +217,10 @@ final class TermuxInstaller {
|
|||
}
|
||||
|
||||
Logger.logInfo(LOG_TAG, "Bootstrap packages installed successfully.");
|
||||
|
||||
// Recreate env file since termux prefix was wiped earlier
|
||||
TermuxShellEnvironment.writeEnvironmentToFile(activity);
|
||||
|
||||
activity.runOnUiThread(whenDone);
|
||||
|
||||
} catch (final Exception e) {
|
||||
|
|
@ -252,14 +264,19 @@ final class TermuxInstaller {
|
|||
}
|
||||
|
||||
private static void sendBootstrapCrashReportNotification(Activity activity, String message) {
|
||||
CrashUtils.sendCrashReportNotification(activity, LOG_TAG,
|
||||
"## Bootstrap Error\n\n" + message + "\n\n" +
|
||||
final String title = TermuxConstants.TERMUX_APP_NAME + " Bootstrap Error";
|
||||
|
||||
// Add info of all install Termux plugin apps as well since their target sdk or installation
|
||||
// on external/portable sd card can affect Termux app files directory access or exec.
|
||||
TermuxCrashUtils.sendCrashReportNotification(activity, LOG_TAG,
|
||||
title, null, "## " + title + "\n\n" + message + "\n\n" +
|
||||
TermuxUtils.getTermuxDebugMarkdownString(activity),
|
||||
true, true);
|
||||
true, false, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGES, true);
|
||||
}
|
||||
|
||||
static void setupStorageSymlinks(final Context context) {
|
||||
final String LOG_TAG = "termux-storage";
|
||||
final String title = TermuxConstants.TERMUX_APP_NAME + " Setup Storage Error";
|
||||
|
||||
Logger.logInfo(LOG_TAG, "Setting up storage symlinks.");
|
||||
|
||||
|
|
@ -273,15 +290,21 @@ final class TermuxInstaller {
|
|||
if (error != null) {
|
||||
Logger.logErrorAndShowToast(context, LOG_TAG, error.getMessage());
|
||||
Logger.logErrorExtended(LOG_TAG, "Setup Storage Error\n" + error.toString());
|
||||
CrashUtils.sendCrashReportNotification(context, LOG_TAG, "## Setup Storage Error\n\n" + Error.getErrorMarkdownString(error), true, true);
|
||||
TermuxCrashUtils.sendCrashReportNotification(context, LOG_TAG, title, null,
|
||||
"## " + title + "\n\n" + Error.getErrorMarkdownString(error),
|
||||
true, false, TermuxUtils.AppInfoMode.TERMUX_PACKAGE, true);
|
||||
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() + "\".");
|
||||
|
||||
// Get primary storage root "/storage/emulated/0" symlink
|
||||
File sharedDir = Environment.getExternalStorageDirectory();
|
||||
Os.symlink(sharedDir.getAbsolutePath(), new File(storageDir, "shared").getAbsolutePath());
|
||||
|
||||
File documentsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
|
||||
Os.symlink(documentsDir.getAbsolutePath(), new File(storageDir, "documents").getAbsolutePath());
|
||||
|
||||
File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
||||
Os.symlink(downloadsDir.getAbsolutePath(), new File(storageDir, "downloads").getAbsolutePath());
|
||||
|
||||
|
|
@ -297,9 +320,25 @@ final class TermuxInstaller {
|
|||
File moviesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
|
||||
Os.symlink(moviesDir.getAbsolutePath(), new File(storageDir, "movies").getAbsolutePath());
|
||||
|
||||
final File[] dirs = context.getExternalFilesDirs(null);
|
||||
if (dirs != null && dirs.length > 1) {
|
||||
for (int i = 1; i < dirs.length; i++) {
|
||||
File podcastsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PODCASTS);
|
||||
Os.symlink(podcastsDir.getAbsolutePath(), new File(storageDir, "podcasts").getAbsolutePath());
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
||||
File audiobooksDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_AUDIOBOOKS);
|
||||
Os.symlink(audiobooksDir.getAbsolutePath(), new File(storageDir, "audiobooks").getAbsolutePath());
|
||||
}
|
||||
|
||||
// Dir 0 should ideally be for primary storage
|
||||
// https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/app/ContextImpl.java;l=818
|
||||
// https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/os/Environment.java;l=219
|
||||
// https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/os/Environment.java;l=181
|
||||
// https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/services/core/java/com/android/server/StorageManagerService.java;l=3796
|
||||
// https://cs.android.com/android/platform/superproject/+/android-7.0.0_r36:frameworks/base/services/core/java/com/android/server/MountService.java;l=3053
|
||||
|
||||
// Create "Android/data/com.termux" symlinks
|
||||
File[] dirs = context.getExternalFilesDirs(null);
|
||||
if (dirs != null && dirs.length > 0) {
|
||||
for (int i = 0; i < dirs.length; i++) {
|
||||
File dir = dirs[i];
|
||||
if (dir == null) continue;
|
||||
String symlinkName = "external-" + i;
|
||||
|
|
@ -308,11 +347,25 @@ final class TermuxInstaller {
|
|||
}
|
||||
}
|
||||
|
||||
// Create "Android/media/com.termux" symlinks
|
||||
dirs = context.getExternalMediaDirs();
|
||||
if (dirs != null && dirs.length > 0) {
|
||||
for (int i = 0; i < dirs.length; i++) {
|
||||
File dir = dirs[i];
|
||||
if (dir == null) continue;
|
||||
String symlinkName = "media-" + i;
|
||||
Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/" + symlinkName + " for \"" + dir.getAbsolutePath() + "\".");
|
||||
Os.symlink(dir.getAbsolutePath(), new File(storageDir, symlinkName).getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
Logger.logInfo(LOG_TAG, "Storage symlinks created successfully.");
|
||||
} catch (Exception e) {
|
||||
Logger.logErrorAndShowToast(context, LOG_TAG, e.getMessage());
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Setup Storage Error: Error setting up link", e);
|
||||
CrashUtils.sendCrashReportNotification(context, LOG_TAG, "## Setup Storage Error\n\n" + Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)), true, true);
|
||||
TermuxCrashUtils.sendCrashReportNotification(context, LOG_TAG, title, null,
|
||||
"## " + title + "\n\n" + Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)),
|
||||
true, false, TermuxUtils.AppInfoMode.TERMUX_PACKAGE, true);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
|
|
|
|||
|
|
@ -13,9 +13,12 @@ import android.os.ParcelFileDescriptor;
|
|||
import android.provider.MediaStore;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import com.termux.app.utils.PluginUtils;
|
||||
import com.termux.shared.termux.plugins.TermuxPluginUtils;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.data.IntentUtils;
|
||||
import com.termux.shared.net.uri.UriUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.net.uri.UriScheme;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
import java.io.File;
|
||||
|
|
@ -32,13 +35,13 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||
public void onReceive(Context context, Intent intent) {
|
||||
final Uri data = intent.getData();
|
||||
if (data == null) {
|
||||
Logger.logError(LOG_TAG, "termux-open: Called without intent data");
|
||||
Logger.logError(LOG_TAG, "Called without intent data");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.logVerbose(LOG_TAG, "Intent Received:\n" + IntentUtils.getIntentString(intent));
|
||||
Logger.logVerbose(LOG_TAG, "uri: \"" + data + "\", path: \"" + data.getPath() + "\", fragment: \"" + data.getFragment() + "\"");
|
||||
|
||||
final String filePath = data.getPath();
|
||||
final String contentTypeExtra = intent.getStringExtra("content-type");
|
||||
final boolean useChooser = intent.getBooleanExtra("chooser", false);
|
||||
final String intentAction = intent.getAction() == null ? Intent.ACTION_VIEW : intent.getAction();
|
||||
|
|
@ -52,8 +55,8 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||
break;
|
||||
}
|
||||
|
||||
final boolean isExternalUrl = data.getScheme() != null && !data.getScheme().equals("file");
|
||||
if (isExternalUrl) {
|
||||
String scheme = data.getScheme();
|
||||
if (scheme != null && !UriScheme.SCHEME_FILE.equals(scheme)) {
|
||||
Intent urlIntent = new Intent(intentAction, data);
|
||||
if (intentAction.equals(Intent.ACTION_SEND)) {
|
||||
urlIntent.putExtra(Intent.EXTRA_TEXT, data.toString());
|
||||
|
|
@ -65,14 +68,21 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||
try {
|
||||
context.startActivity(urlIntent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Logger.logError(LOG_TAG, "termux-open: No app handles the url " + data);
|
||||
Logger.logError(LOG_TAG, "No app handles the url " + data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Get full path including fragment (anything after last "#")
|
||||
String filePath = UriUtils.getUriFilePathWithFragment(data);
|
||||
if (DataUtils.isNullOrEmpty(filePath)) {
|
||||
Logger.logError(LOG_TAG, "filePath is null or empty");
|
||||
return;
|
||||
}
|
||||
|
||||
final File fileToShare = new File(filePath);
|
||||
if (!(fileToShare.isFile() && fileToShare.canRead())) {
|
||||
Logger.logError(LOG_TAG, "termux-open: Not a readable file: '" + fileToShare.getAbsolutePath() + "'");
|
||||
Logger.logError(LOG_TAG, "Not a readable file: '" + fileToShare.getAbsolutePath() + "'");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +103,8 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||
contentTypeToUse = contentTypeExtra;
|
||||
}
|
||||
|
||||
Uri uriToShare = Uri.parse("content://" + TermuxConstants.TERMUX_FILE_SHARE_URI_AUTHORITY + fileToShare.getAbsolutePath());
|
||||
// Do not create Uri with Uri.parse() and use Uri.Builder().path(), check UriUtils.getUriFilePath().
|
||||
Uri uriToShare = UriUtils.getContentUri(TermuxConstants.TERMUX_FILE_SHARE_URI_AUTHORITY, fileToShare.getAbsolutePath());
|
||||
|
||||
if (Intent.ACTION_SEND.equals(intentAction)) {
|
||||
sendIntent.putExtra(Intent.EXTRA_STREAM, uriToShare);
|
||||
|
|
@ -109,7 +120,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||
try {
|
||||
context.startActivity(sendIntent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Logger.logError(LOG_TAG, "termux-open: No app handles the url " + data);
|
||||
Logger.logError(LOG_TAG, "No app handles the url " + data);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -161,6 +172,13 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||
|
||||
@Override
|
||||
public String getType(@NonNull Uri uri) {
|
||||
String path = uri.getLastPathSegment();
|
||||
int extIndex = path.lastIndexOf('.') + 1;
|
||||
if (extIndex > 0) {
|
||||
MimeTypeMap mimeMap = MimeTypeMap.getSingleton();
|
||||
String ext = path.substring(extIndex).toLowerCase();
|
||||
return mimeMap.getMimeTypeFromExtension(ext);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -184,25 +202,25 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||
File file = new File(uri.getPath());
|
||||
try {
|
||||
String path = file.getCanonicalPath();
|
||||
Logger.logDebug(LOG_TAG, "Open file request received for \"" + path + "\" with mode \"" + mode + "\"");
|
||||
String callingPackageName = getCallingPackage();
|
||||
Logger.logDebug(LOG_TAG, "Open file request received from " + callingPackageName + " for \"" + path + "\" with mode \"" + mode + "\"");
|
||||
String storagePath = Environment.getExternalStorageDirectory().getCanonicalPath();
|
||||
// See https://support.google.com/faqs/answer/7496913:
|
||||
if (!(path.startsWith(TermuxConstants.TERMUX_FILES_DIR_PATH) || path.startsWith(storagePath))) {
|
||||
throw new IllegalArgumentException("Invalid path: " + path);
|
||||
}
|
||||
|
||||
// If "allow-external-apps" property to not set to "true", then throw exception
|
||||
String errmsg = PluginUtils.checkIfAllowExternalAppsPolicyIsViolated(getContext(), LOG_TAG);
|
||||
// If TermuxConstants.PROP_ALLOW_EXTERNAL_APPS property to not set to "true", then throw exception
|
||||
String errmsg = TermuxPluginUtils.checkIfAllowExternalAppsPolicyIsViolated(getContext(), LOG_TAG);
|
||||
if (errmsg != null) {
|
||||
throw new IllegalArgumentException(errmsg);
|
||||
}
|
||||
|
||||
// Do not allow apps with RUN_COMMAND permission to modify termux apps properties files,
|
||||
// including allow-external-apps
|
||||
if (TermuxConstants.TERMUX_PROPERTIES_PRIMARY_FILE_PATH.equals(path) ||
|
||||
TermuxConstants.TERMUX_PROPERTIES_SECONDARY_FILE_PATH.equals(path) ||
|
||||
TermuxConstants.TERMUX_FLOAT_PROPERTIES_PRIMARY_FILE_PATH.equals(path) ||
|
||||
TermuxConstants.TERMUX_FLOAT_PROPERTIES_SECONDARY_FILE_PATH.equals(path)) {
|
||||
// **DO NOT** allow these files to be modified by ContentProvider exposed to external
|
||||
// apps, since they may silently modify the values for security properties like
|
||||
// TermuxConstants.PROP_ALLOW_EXTERNAL_APPS set by users without their explicit consent.
|
||||
if (TermuxConstants.TERMUX_PROPERTIES_FILE_PATHS_LIST.contains(path) ||
|
||||
TermuxConstants.TERMUX_FLOAT_PROPERTIES_FILE_PATHS_LIST.contains(path)) {
|
||||
mode = "r";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,43 +5,46 @@ import android.app.Notification;
|
|||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.net.Uri;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.PowerManager;
|
||||
import android.provider.Settings;
|
||||
import android.widget.ArrayAdapter;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.settings.properties.TermuxAppSharedProperties;
|
||||
import com.termux.app.terminal.TermuxTerminalSessionClient;
|
||||
import com.termux.app.utils.PluginUtils;
|
||||
import com.termux.app.event.SystemEventReceiver;
|
||||
import com.termux.app.terminal.TermuxTerminalSessionActivityClient;
|
||||
import com.termux.app.terminal.TermuxTerminalSessionServiceClient;
|
||||
import com.termux.shared.termux.plugins.TermuxPluginUtils;
|
||||
import com.termux.shared.data.IntentUtils;
|
||||
import com.termux.shared.models.errors.Errno;
|
||||
import com.termux.shared.net.uri.UriUtils;
|
||||
import com.termux.shared.errors.Errno;
|
||||
import com.termux.shared.shell.ShellUtils;
|
||||
import com.termux.shared.shell.TermuxShellEnvironmentClient;
|
||||
import com.termux.shared.shell.TermuxShellUtils;
|
||||
import com.termux.shared.shell.command.runner.app.AppShell;
|
||||
import com.termux.shared.termux.settings.properties.TermuxAppSharedProperties;
|
||||
import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment;
|
||||
import com.termux.shared.termux.shell.TermuxShellUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.shell.TermuxSession;
|
||||
import com.termux.shared.terminal.TermuxTerminalSessionClientBase;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.termux.shell.TermuxShellManager;
|
||||
import com.termux.shared.termux.shell.command.runner.terminal.TermuxSession;
|
||||
import com.termux.shared.termux.terminal.TermuxTerminalSessionClientBase;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.notification.NotificationUtils;
|
||||
import com.termux.shared.packages.PermissionUtils;
|
||||
import com.termux.shared.android.PermissionUtils;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.models.ExecutionCommand;
|
||||
import com.termux.shared.shell.TermuxTask;
|
||||
import com.termux.shared.shell.command.ExecutionCommand;
|
||||
import com.termux.shared.shell.command.ExecutionCommand.Runner;
|
||||
import com.termux.shared.shell.command.ExecutionCommand.ShellCreateMode;
|
||||
import com.termux.terminal.TerminalEmulator;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
import com.termux.terminal.TerminalSessionClient;
|
||||
|
|
@ -50,8 +53,8 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A service holding a list of {@link TermuxSession} in {@link #mTermuxSessions} and background {@link TermuxTask}
|
||||
* in {@link #mTermuxTasks}, showing a foreground notification while running so that it is not terminated.
|
||||
* A service holding a list of {@link TermuxSession} in {@link TermuxShellManager#mTermuxSessions} and background {@link AppShell}
|
||||
* in {@link TermuxShellManager#mTermuxTasks}, showing a foreground notification while running so that it is not terminated.
|
||||
* The user interacts with the session through {@link TermuxActivity}, but this service may outlive
|
||||
* the activity when the user or the system disposes of the activity. In that case the user may
|
||||
* restart {@link TermuxActivity} later to yet again access the sessions.
|
||||
|
|
@ -62,9 +65,7 @@ import java.util.List;
|
|||
* Optionally may hold a wake and a wifi lock, in which case that is shown in the notification - see
|
||||
* {@link #buildNotification()}.
|
||||
*/
|
||||
public final class TermuxService extends Service implements TermuxTask.TermuxTaskClient, TermuxSession.TermuxSessionClient {
|
||||
|
||||
private static int EXECUTION_ID = 1000;
|
||||
public final class TermuxService extends Service implements AppShell.AppShellClient, TermuxSession.TermuxSessionClient {
|
||||
|
||||
/** This service is only bound from inside the same process and never uses IPC. */
|
||||
class LocalBinder extends Binder {
|
||||
|
|
@ -75,34 +76,27 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||
|
||||
private final Handler mHandler = new Handler();
|
||||
|
||||
/**
|
||||
* The foreground TermuxSessions which this service manages.
|
||||
* Note that this list is observed by {@link TermuxActivity#mTermuxSessionListViewController},
|
||||
* so any changes must be made on the UI thread and followed by a call to
|
||||
* {@link ArrayAdapter#notifyDataSetChanged()} }.
|
||||
*/
|
||||
final List<TermuxSession> mTermuxSessions = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* The background TermuxTasks which this service manages.
|
||||
*/
|
||||
final List<TermuxTask> mTermuxTasks = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* The pending plugin ExecutionCommands that have yet to be processed by this service.
|
||||
*/
|
||||
final List<ExecutionCommand> mPendingPluginExecutionCommands = new ArrayList<>();
|
||||
|
||||
/** The full implementation of the {@link TerminalSessionClient} interface to be used by {@link TerminalSession}
|
||||
* that holds activity references for activity related functions.
|
||||
* Note that the service may often outlive the activity, so need to clear this reference.
|
||||
*/
|
||||
TermuxTerminalSessionClient mTermuxTerminalSessionClient;
|
||||
private TermuxTerminalSessionActivityClient mTermuxTerminalSessionActivityClient;
|
||||
|
||||
/** The basic implementation of the {@link TerminalSessionClient} interface to be used by {@link TerminalSession}
|
||||
* that does not hold activity references.
|
||||
* that does not hold activity references and only a service reference.
|
||||
*/
|
||||
final TermuxTerminalSessionClientBase mTermuxTerminalSessionClientBase = new TermuxTerminalSessionClientBase();
|
||||
private final TermuxTerminalSessionServiceClient mTermuxTerminalSessionServiceClient = new TermuxTerminalSessionServiceClient(this);
|
||||
|
||||
/**
|
||||
* Termux app shared properties manager, loaded from termux.properties
|
||||
*/
|
||||
private TermuxAppSharedProperties mProperties;
|
||||
|
||||
/**
|
||||
* Termux app shell manager
|
||||
*/
|
||||
private TermuxShellManager mShellManager;
|
||||
|
||||
/** The wake lock and wifi lock are always acquired and released together. */
|
||||
private PowerManager.WakeLock mWakeLock;
|
||||
|
|
@ -111,14 +105,21 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||
/** If the user has executed the {@link TERMUX_SERVICE#ACTION_STOP_SERVICE} intent. */
|
||||
boolean mWantsToStop = false;
|
||||
|
||||
public Integer mTerminalTranscriptRows;
|
||||
|
||||
private static final String LOG_TAG = "TermuxService";
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
Logger.logVerbose(LOG_TAG, "onCreate");
|
||||
|
||||
// Get Termux app SharedProperties without loading from disk since TermuxApplication handles
|
||||
// load and TermuxActivity handles reloads
|
||||
mProperties = TermuxAppSharedProperties.getProperties();
|
||||
|
||||
mShellManager = TermuxShellManager.getShellManager();
|
||||
|
||||
runStartForeground();
|
||||
|
||||
SystemEventReceiver.registerPackageUpdateEvents(this);
|
||||
}
|
||||
|
||||
@SuppressLint("Wakelock")
|
||||
|
|
@ -129,7 +130,11 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||
// Run again in case service is already started and onCreate() is not called
|
||||
runStartForeground();
|
||||
|
||||
String action = intent.getAction();
|
||||
String action = null;
|
||||
if (intent != null) {
|
||||
Logger.logVerboseExtended(LOG_TAG, "Intent Received:\n" + IntentUtils.getIntentString(intent));
|
||||
action = intent.getAction();
|
||||
}
|
||||
|
||||
if (action != null) {
|
||||
switch (action) {
|
||||
|
|
@ -169,6 +174,11 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||
actionReleaseWakeLock(false);
|
||||
if (!mWantsToStop)
|
||||
killAllTermuxExecutionCommands();
|
||||
|
||||
TermuxShellManager.onAppExit(this);
|
||||
|
||||
SystemEventReceiver.unregisterPackageUpdateEvents(this);
|
||||
|
||||
runStopForeground();
|
||||
}
|
||||
|
||||
|
|
@ -185,7 +195,7 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||
// Since we cannot rely on {@link TermuxActivity.onDestroy()} to always complete,
|
||||
// we unset clients here as well if it failed, so that we do not leave service and session
|
||||
// clients with references to the activity.
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
unsetTermuxTerminalSessionClient();
|
||||
return false;
|
||||
}
|
||||
|
|
@ -253,28 +263,36 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||
private synchronized void killAllTermuxExecutionCommands() {
|
||||
boolean processResult;
|
||||
|
||||
Logger.logDebug(LOG_TAG, "Killing TermuxSessions=" + mTermuxSessions.size() + ", TermuxTasks=" + mTermuxTasks.size() + ", PendingPluginExecutionCommands=" + mPendingPluginExecutionCommands.size());
|
||||
Logger.logDebug(LOG_TAG, "Killing TermuxSessions=" + mShellManager.mTermuxSessions.size() +
|
||||
", TermuxTasks=" + mShellManager.mTermuxTasks.size() +
|
||||
", PendingPluginExecutionCommands=" + mShellManager.mPendingPluginExecutionCommands.size());
|
||||
|
||||
List<TermuxSession> termuxSessions = new ArrayList<>(mShellManager.mTermuxSessions);
|
||||
List<AppShell> termuxTasks = new ArrayList<>(mShellManager.mTermuxTasks);
|
||||
List<ExecutionCommand> pendingPluginExecutionCommands = new ArrayList<>(mShellManager.mPendingPluginExecutionCommands);
|
||||
|
||||
List<TermuxSession> termuxSessions = new ArrayList<>(mTermuxSessions);
|
||||
for (int i = 0; i < termuxSessions.size(); i++) {
|
||||
ExecutionCommand executionCommand = termuxSessions.get(i).getExecutionCommand();
|
||||
processResult = mWantsToStop || executionCommand.isPluginExecutionCommandWithPendingResult();
|
||||
termuxSessions.get(i).killIfExecuting(this, processResult);
|
||||
if (!processResult)
|
||||
mShellManager.mTermuxSessions.remove(termuxSessions.get(i));
|
||||
}
|
||||
|
||||
List<TermuxTask> termuxTasks = new ArrayList<>(mTermuxTasks);
|
||||
|
||||
for (int i = 0; i < termuxTasks.size(); i++) {
|
||||
ExecutionCommand executionCommand = termuxTasks.get(i).getExecutionCommand();
|
||||
if (executionCommand.isPluginExecutionCommandWithPendingResult())
|
||||
termuxTasks.get(i).killIfExecuting(this, true);
|
||||
else
|
||||
mShellManager.mTermuxTasks.remove(termuxTasks.get(i));
|
||||
}
|
||||
|
||||
List<ExecutionCommand> pendingPluginExecutionCommands = new ArrayList<>(mPendingPluginExecutionCommands);
|
||||
for (int i = 0; i < pendingPluginExecutionCommands.size(); i++) {
|
||||
ExecutionCommand executionCommand = pendingPluginExecutionCommands.get(i);
|
||||
if (!executionCommand.shouldNotProcessResults() && executionCommand.isPluginExecutionCommandWithPendingResult()) {
|
||||
if (executionCommand.setStateFailed(Errno.ERRNO_CANCELLED.getCode(), this.getString(com.termux.shared.R.string.error_execution_cancelled))) {
|
||||
PluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
|
||||
TermuxPluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -301,18 +319,8 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||
mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, TermuxConstants.TERMUX_APP_NAME.toLowerCase());
|
||||
mWifiLock.acquire();
|
||||
|
||||
String packageName = getPackageName();
|
||||
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
|
||||
Intent whitelist = new Intent();
|
||||
whitelist.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||
whitelist.setData(Uri.parse("package:" + packageName));
|
||||
whitelist.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
try {
|
||||
startActivity(whitelist);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to call ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS", e);
|
||||
}
|
||||
if (!PermissionUtils.checkIfBatteryOptimizationsDisabled(this)) {
|
||||
PermissionUtils.requestDisableBatteryOptimizations(this);
|
||||
}
|
||||
|
||||
updateNotification();
|
||||
|
|
@ -354,27 +362,41 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||
return;
|
||||
}
|
||||
|
||||
ExecutionCommand executionCommand = new ExecutionCommand(getNextExecutionId());
|
||||
ExecutionCommand executionCommand = new ExecutionCommand(TermuxShellManager.getNextShellId());
|
||||
|
||||
executionCommand.executableUri = intent.getData();
|
||||
executionCommand.inBackground = intent.getBooleanExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, false);
|
||||
executionCommand.isPluginExecutionCommand = true;
|
||||
|
||||
// If EXTRA_RUNNER is passed, use that, otherwise check EXTRA_BACKGROUND and default to Runner.TERMINAL_SESSION
|
||||
executionCommand.runner = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RUNNER,
|
||||
(intent.getBooleanExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, false) ? Runner.APP_SHELL.getName() : Runner.TERMINAL_SESSION.getName()));
|
||||
if (Runner.runnerOf(executionCommand.runner) == null) {
|
||||
String errmsg = this.getString(R.string.error_termux_service_invalid_execution_command_runner, executionCommand.runner);
|
||||
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (executionCommand.executableUri != null) {
|
||||
executionCommand.executable = executionCommand.executableUri.getPath();
|
||||
Logger.logVerbose(LOG_TAG, "uri: \"" + executionCommand.executableUri + "\", path: \"" + executionCommand.executableUri.getPath() + "\", fragment: \"" + executionCommand.executableUri.getFragment() + "\"");
|
||||
|
||||
// Get full path including fragment (anything after last "#")
|
||||
executionCommand.executable = UriUtils.getUriFilePathWithFragment(executionCommand.executableUri);
|
||||
executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, TERMUX_SERVICE.EXTRA_ARGUMENTS, null);
|
||||
if (executionCommand.inBackground)
|
||||
if (Runner.APP_SHELL.equalsRunner(executionCommand.runner))
|
||||
executionCommand.stdin = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_STDIN, null);
|
||||
executionCommand.backgroundCustomLogLevel = IntentUtils.getIntegerExtraIfSet(intent, TERMUX_SERVICE.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL, null);
|
||||
executionCommand.backgroundCustomLogLevel = IntentUtils.getIntegerExtraIfSet(intent, TERMUX_SERVICE.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL, null);
|
||||
}
|
||||
|
||||
executionCommand.workingDirectory = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_WORKDIR, null);
|
||||
executionCommand.isFailsafe = intent.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false);
|
||||
executionCommand.sessionAction = intent.getStringExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION);
|
||||
executionCommand.shellName = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_SHELL_NAME, null);
|
||||
executionCommand.shellCreateMode = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_SHELL_CREATE_MODE, null);
|
||||
executionCommand.commandLabel = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_LABEL, "Execution Intent Command");
|
||||
executionCommand.commandDescription = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, null);
|
||||
executionCommand.commandHelp = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_HELP, null);
|
||||
executionCommand.pluginAPIHelp = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP, null);
|
||||
executionCommand.isPluginExecutionCommand = true;
|
||||
executionCommand.resultConfig.resultPendingIntent = intent.getParcelableExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT);
|
||||
executionCommand.resultConfig.resultDirectoryPath = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_DIRECTORY, null);
|
||||
if (executionCommand.resultConfig.resultDirectoryPath != null) {
|
||||
|
|
@ -385,13 +407,20 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||
executionCommand.resultConfig.resultFilesSuffix = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILES_SUFFIX, null);
|
||||
}
|
||||
|
||||
// Add the execution command to pending plugin execution commands list
|
||||
mPendingPluginExecutionCommands.add(executionCommand);
|
||||
if (executionCommand.shellCreateMode == null)
|
||||
executionCommand.shellCreateMode = ShellCreateMode.ALWAYS.getMode();
|
||||
|
||||
if (executionCommand.inBackground) {
|
||||
// Add the execution command to pending plugin execution commands list
|
||||
mShellManager.mPendingPluginExecutionCommands.add(executionCommand);
|
||||
|
||||
if (Runner.APP_SHELL.equalsRunner(executionCommand.runner))
|
||||
executeTermuxTaskCommand(executionCommand);
|
||||
} else {
|
||||
else if (Runner.TERMINAL_SESSION.equalsRunner(executionCommand.runner))
|
||||
executeTermuxSessionCommand(executionCommand);
|
||||
else {
|
||||
String errmsg = getString(R.string.error_termux_service_unsupported_execution_command_runner, executionCommand.runner);
|
||||
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -399,62 +428,84 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||
|
||||
|
||||
|
||||
/** Execute a shell command in background {@link TermuxTask}. */
|
||||
/** Execute a shell command in background TermuxTask. */
|
||||
private void executeTermuxTaskCommand(ExecutionCommand executionCommand) {
|
||||
if (executionCommand == null) return;
|
||||
|
||||
Logger.logDebug(LOG_TAG, "Executing background \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask command");
|
||||
|
||||
TermuxTask newTermuxTask = createTermuxTask(executionCommand);
|
||||
// Transform executable path to shell/session name, e.g. "/bin/do-something.sh" => "do-something.sh".
|
||||
if (executionCommand.shellName == null && executionCommand.executable != null)
|
||||
executionCommand.shellName = ShellUtils.getExecutableBasename(executionCommand.executable);
|
||||
|
||||
AppShell newTermuxTask = null;
|
||||
ShellCreateMode shellCreateMode = processShellCreateMode(executionCommand);
|
||||
if (shellCreateMode == null) return;
|
||||
if (ShellCreateMode.NO_SHELL_WITH_NAME.equals(shellCreateMode)) {
|
||||
newTermuxTask = getTermuxTaskForShellName(executionCommand.shellName);
|
||||
if (newTermuxTask != null)
|
||||
Logger.logVerbose(LOG_TAG, "Existing TermuxTask with \"" + executionCommand.shellName + "\" shell name found for shell create mode \"" + shellCreateMode.getMode() + "\"");
|
||||
else
|
||||
Logger.logVerbose(LOG_TAG, "No existing TermuxTask with \"" + executionCommand.shellName + "\" shell name found for shell create mode \"" + shellCreateMode.getMode() + "\"");
|
||||
}
|
||||
|
||||
if (newTermuxTask == null)
|
||||
newTermuxTask = createTermuxTask(executionCommand);
|
||||
}
|
||||
|
||||
/** Create a {@link TermuxTask}. */
|
||||
/** Create a TermuxTask. */
|
||||
@Nullable
|
||||
public TermuxTask createTermuxTask(String executablePath, String[] arguments, String stdin, String workingDirectory) {
|
||||
return createTermuxTask(new ExecutionCommand(getNextExecutionId(), executablePath, arguments, stdin, workingDirectory, true, false));
|
||||
public AppShell createTermuxTask(String executablePath, String[] arguments, String stdin, String workingDirectory) {
|
||||
return createTermuxTask(new ExecutionCommand(TermuxShellManager.getNextShellId(), executablePath,
|
||||
arguments, stdin, workingDirectory, Runner.APP_SHELL.getName(), false));
|
||||
}
|
||||
|
||||
/** Create a {@link TermuxTask}. */
|
||||
/** Create a TermuxTask. */
|
||||
@Nullable
|
||||
public synchronized TermuxTask createTermuxTask(ExecutionCommand executionCommand) {
|
||||
public synchronized AppShell createTermuxTask(ExecutionCommand executionCommand) {
|
||||
if (executionCommand == null) return null;
|
||||
|
||||
Logger.logDebug(LOG_TAG, "Creating \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask");
|
||||
|
||||
if (!executionCommand.inBackground) {
|
||||
Logger.logDebug(LOG_TAG, "Ignoring a foreground execution command passed to createTermuxTask()");
|
||||
if (!Runner.APP_SHELL.equalsRunner(executionCommand.runner)) {
|
||||
Logger.logDebug(LOG_TAG, "Ignoring wrong runner \"" + executionCommand.runner + "\" command passed to createTermuxTask()");
|
||||
return null;
|
||||
}
|
||||
|
||||
executionCommand.setShellCommandShellEnvironment = true;
|
||||
|
||||
if (Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE)
|
||||
Logger.logVerboseExtended(LOG_TAG, executionCommand.toString());
|
||||
|
||||
TermuxTask newTermuxTask = TermuxTask.execute(this, executionCommand, this, new TermuxShellEnvironmentClient(), false);
|
||||
AppShell newTermuxTask = AppShell.execute(this, executionCommand, this,
|
||||
new TermuxShellEnvironment(), null,false);
|
||||
if (newTermuxTask == null) {
|
||||
Logger.logError(LOG_TAG, "Failed to execute new TermuxTask command for:\n" + executionCommand.getCommandIdAndLabelLogString());
|
||||
// If the execution command was started for a plugin, then process the error
|
||||
if (executionCommand.isPluginExecutionCommand)
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
else
|
||||
Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
|
||||
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
else {
|
||||
Logger.logError(LOG_TAG, "Set log level to debug or higher to see error in logs");
|
||||
Logger.logErrorPrivateExtended(LOG_TAG, executionCommand.toString());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
mTermuxTasks.add(newTermuxTask);
|
||||
mShellManager.mTermuxTasks.add(newTermuxTask);
|
||||
|
||||
// Remove the execution command from the pending plugin execution commands list since it has
|
||||
// now been processed
|
||||
if (executionCommand.isPluginExecutionCommand)
|
||||
mPendingPluginExecutionCommands.remove(executionCommand);
|
||||
mShellManager.mPendingPluginExecutionCommands.remove(executionCommand);
|
||||
|
||||
updateNotification();
|
||||
|
||||
return newTermuxTask;
|
||||
}
|
||||
|
||||
/** Callback received when a {@link TermuxTask} finishes. */
|
||||
/** Callback received when a TermuxTask finishes. */
|
||||
@Override
|
||||
public void onTermuxTaskExited(final TermuxTask termuxTask) {
|
||||
public void onAppShellExited(final AppShell termuxTask) {
|
||||
mHandler.post(() -> {
|
||||
if (termuxTask != null) {
|
||||
ExecutionCommand executionCommand = termuxTask.getExecutionCommand();
|
||||
|
|
@ -463,9 +514,9 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||
|
||||
// If the execution command was started for a plugin, then process the results
|
||||
if (executionCommand != null && executionCommand.isPluginExecutionCommand)
|
||||
PluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
|
||||
TermuxPluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
|
||||
|
||||
mTermuxTasks.remove(termuxTask);
|
||||
mShellManager.mTermuxTasks.remove(termuxTask);
|
||||
}
|
||||
|
||||
updateNotification();
|
||||
|
|
@ -482,14 +533,23 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||
|
||||
Logger.logDebug(LOG_TAG, "Executing foreground \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession command");
|
||||
|
||||
String sessionName = null;
|
||||
// Transform executable path to shell/session name, e.g. "/bin/do-something.sh" => "do-something.sh".
|
||||
if (executionCommand.shellName == null && executionCommand.executable != null)
|
||||
executionCommand.shellName = ShellUtils.getExecutableBasename(executionCommand.executable);
|
||||
|
||||
// Transform executable path to session name, e.g. "/bin/do-something.sh" => "do something.sh".
|
||||
if (executionCommand.executable != null) {
|
||||
sessionName = ShellUtils.getExecutableBasename(executionCommand.executable).replace('-', ' ');
|
||||
TermuxSession newTermuxSession = null;
|
||||
ShellCreateMode shellCreateMode = processShellCreateMode(executionCommand);
|
||||
if (shellCreateMode == null) return;
|
||||
if (ShellCreateMode.NO_SHELL_WITH_NAME.equals(shellCreateMode)) {
|
||||
newTermuxSession = getTermuxSessionForShellName(executionCommand.shellName);
|
||||
if (newTermuxSession != null)
|
||||
Logger.logVerbose(LOG_TAG, "Existing TermuxSession with \"" + executionCommand.shellName + "\" shell name found for shell create mode \"" + shellCreateMode.getMode() + "\"");
|
||||
else
|
||||
Logger.logVerbose(LOG_TAG, "No existing TermuxSession with \"" + executionCommand.shellName + "\" shell name found for shell create mode \"" + shellCreateMode.getMode() + "\"");
|
||||
}
|
||||
|
||||
TermuxSession newTermuxSession = createTermuxSession(executionCommand, sessionName);
|
||||
if (newTermuxSession == null)
|
||||
newTermuxSession = createTermuxSession(executionCommand);
|
||||
if (newTermuxSession == null) return;
|
||||
|
||||
handleSessionAction(DataUtils.getIntFromString(executionCommand.sessionAction,
|
||||
|
|
@ -499,57 +559,68 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||
|
||||
/**
|
||||
* Create a {@link TermuxSession}.
|
||||
* Currently called by {@link TermuxTerminalSessionClient#addNewSession(boolean, String)} to add a new {@link TermuxSession}.
|
||||
* Currently called by {@link TermuxTerminalSessionActivityClient#addNewSession(boolean, String)} to add a new {@link TermuxSession}.
|
||||
*/
|
||||
@Nullable
|
||||
public TermuxSession createTermuxSession(String executablePath, String[] arguments, String stdin, String workingDirectory, boolean isFailSafe, String sessionName) {
|
||||
return createTermuxSession(new ExecutionCommand(getNextExecutionId(), executablePath, arguments, stdin, workingDirectory, false, isFailSafe), sessionName);
|
||||
public TermuxSession createTermuxSession(String executablePath, String[] arguments, String stdin,
|
||||
String workingDirectory, boolean isFailSafe, String sessionName) {
|
||||
ExecutionCommand executionCommand = new ExecutionCommand(TermuxShellManager.getNextShellId(),
|
||||
executablePath, arguments, stdin, workingDirectory, Runner.TERMINAL_SESSION.getName(), isFailSafe);
|
||||
executionCommand.shellName = sessionName;
|
||||
return createTermuxSession(executionCommand);
|
||||
}
|
||||
|
||||
/** Create a {@link TermuxSession}. */
|
||||
@Nullable
|
||||
public synchronized TermuxSession createTermuxSession(ExecutionCommand executionCommand, String sessionName) {
|
||||
public synchronized TermuxSession createTermuxSession(ExecutionCommand executionCommand) {
|
||||
if (executionCommand == null) return null;
|
||||
|
||||
Logger.logDebug(LOG_TAG, "Creating \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession");
|
||||
|
||||
if (executionCommand.inBackground) {
|
||||
Logger.logDebug(LOG_TAG, "Ignoring a background execution command passed to createTermuxSession()");
|
||||
if (!Runner.TERMINAL_SESSION.equalsRunner(executionCommand.runner)) {
|
||||
Logger.logDebug(LOG_TAG, "Ignoring wrong runner \"" + executionCommand.runner + "\" command passed to createTermuxSession()");
|
||||
return null;
|
||||
}
|
||||
|
||||
executionCommand.setShellCommandShellEnvironment = true;
|
||||
executionCommand.terminalTranscriptRows = mProperties.getTerminalTranscriptRows();
|
||||
|
||||
if (Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE)
|
||||
Logger.logVerboseExtended(LOG_TAG, executionCommand.toString());
|
||||
|
||||
// If the execution command was started for a plugin, only then will the stdout be set
|
||||
// Otherwise if command was manually started by the user like by adding a new terminal session,
|
||||
// then no need to set stdout
|
||||
executionCommand.terminalTranscriptRows = getTerminalTranscriptRows();
|
||||
TermuxSession newTermuxSession = TermuxSession.execute(this, executionCommand, getTermuxTerminalSessionClient(), this, new TermuxShellEnvironmentClient(), sessionName, executionCommand.isPluginExecutionCommand);
|
||||
TermuxSession newTermuxSession = TermuxSession.execute(this, executionCommand, getTermuxTerminalSessionClient(),
|
||||
this, new TermuxShellEnvironment(), null, executionCommand.isPluginExecutionCommand);
|
||||
if (newTermuxSession == null) {
|
||||
Logger.logError(LOG_TAG, "Failed to execute new TermuxSession command for:\n" + executionCommand.getCommandIdAndLabelLogString());
|
||||
// If the execution command was started for a plugin, then process the error
|
||||
if (executionCommand.isPluginExecutionCommand)
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
else
|
||||
Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
|
||||
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
else {
|
||||
Logger.logError(LOG_TAG, "Set log level to debug or higher to see error in logs");
|
||||
Logger.logErrorPrivateExtended(LOG_TAG, executionCommand.toString());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
mTermuxSessions.add(newTermuxSession);
|
||||
mShellManager.mTermuxSessions.add(newTermuxSession);
|
||||
|
||||
// Remove the execution command from the pending plugin execution commands list since it has
|
||||
// now been processed
|
||||
if (executionCommand.isPluginExecutionCommand)
|
||||
mPendingPluginExecutionCommands.remove(executionCommand);
|
||||
mShellManager.mPendingPluginExecutionCommands.remove(executionCommand);
|
||||
|
||||
// Notify {@link TermuxSessionsListViewController} that sessions list has been updated if
|
||||
// activity in is foreground
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.termuxSessionListNotifyUpdated();
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.termuxSessionListNotifyUpdated();
|
||||
|
||||
updateNotification();
|
||||
TermuxActivity.updateTermuxActivityStyling(this);
|
||||
|
||||
// No need to recreate the activity since it likely just started and theme should already have applied
|
||||
TermuxActivity.updateTermuxActivityStyling(this, false);
|
||||
|
||||
return newTermuxSession;
|
||||
}
|
||||
|
|
@ -559,7 +630,7 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||
int index = getIndexOfSession(sessionToRemove);
|
||||
|
||||
if (index >= 0)
|
||||
mTermuxSessions.get(index).finish();
|
||||
mShellManager.mTermuxSessions.get(index).finish();
|
||||
|
||||
return index;
|
||||
}
|
||||
|
|
@ -574,36 +645,41 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||
|
||||
// If the execution command was started for a plugin, then process the results
|
||||
if (executionCommand != null && executionCommand.isPluginExecutionCommand)
|
||||
PluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
|
||||
TermuxPluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
|
||||
|
||||
mTermuxSessions.remove(termuxSession);
|
||||
mShellManager.mTermuxSessions.remove(termuxSession);
|
||||
|
||||
// Notify {@link TermuxSessionsListViewController} that sessions list has been updated if
|
||||
// activity in is foreground
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.termuxSessionListNotifyUpdated();
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.termuxSessionListNotifyUpdated();
|
||||
}
|
||||
|
||||
updateNotification();
|
||||
}
|
||||
|
||||
/** Get the terminal transcript rows to be used for new {@link TermuxSession}. */
|
||||
public Integer getTerminalTranscriptRows() {
|
||||
if (mTerminalTranscriptRows == null)
|
||||
setTerminalTranscriptRows();
|
||||
return mTerminalTranscriptRows;
|
||||
|
||||
|
||||
|
||||
|
||||
private ShellCreateMode processShellCreateMode(@NonNull ExecutionCommand executionCommand) {
|
||||
if (ShellCreateMode.ALWAYS.equalsMode(executionCommand.shellCreateMode))
|
||||
return ShellCreateMode.ALWAYS; // Default
|
||||
else if (ShellCreateMode.NO_SHELL_WITH_NAME.equalsMode(executionCommand.shellCreateMode))
|
||||
if (DataUtils.isNullOrEmpty(executionCommand.shellName)) {
|
||||
TermuxPluginUtils.setAndProcessPluginExecutionCommandError(this, LOG_TAG, executionCommand, false,
|
||||
getString(R.string.error_termux_service_execution_command_shell_name_unset, executionCommand.shellCreateMode));
|
||||
return null;
|
||||
} else {
|
||||
return ShellCreateMode.NO_SHELL_WITH_NAME;
|
||||
}
|
||||
else {
|
||||
TermuxPluginUtils.setAndProcessPluginExecutionCommandError(this, LOG_TAG, executionCommand, false,
|
||||
getString(R.string.error_termux_service_unsupported_execution_command_shell_create_mode, executionCommand.shellCreateMode));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setTerminalTranscriptRows() {
|
||||
// TermuxService only uses this termux property currently, so no need to load them all into
|
||||
// an internal values map like TermuxActivity does
|
||||
mTerminalTranscriptRows = TermuxAppSharedProperties.getTerminalTranscriptRows(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/** Process session action for new session. */
|
||||
private void handleSessionAction(int sessionAction, TerminalSession newTerminalSession) {
|
||||
Logger.logDebug(LOG_TAG, "Processing sessionAction \"" + sessionAction + "\" for session \"" + newTerminalSession.mSessionName + "\"");
|
||||
|
|
@ -611,8 +687,8 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||
switch (sessionAction) {
|
||||
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY:
|
||||
setCurrentStoredTerminalSession(newTerminalSession);
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.setCurrentSession(newTerminalSession);
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.setCurrentSession(newTerminalSession);
|
||||
startTermuxActivity();
|
||||
break;
|
||||
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_OPEN_ACTIVITY:
|
||||
|
|
@ -622,8 +698,8 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||
break;
|
||||
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_DONT_OPEN_ACTIVITY:
|
||||
setCurrentStoredTerminalSession(newTerminalSession);
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.setCurrentSession(newTerminalSession);
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.setCurrentSession(newTerminalSession);
|
||||
break;
|
||||
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_DONT_OPEN_ACTIVITY:
|
||||
if (getTermuxSessionsSize() == 1)
|
||||
|
|
@ -646,8 +722,8 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||
} else {
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(this);
|
||||
if (preferences == null) return;
|
||||
if (preferences.arePluginErrorNotificationsEnabled())
|
||||
Logger.showToast(this, this.getString(R.string.error_display_over_other_apps_permission_not_granted), true);
|
||||
if (preferences.arePluginErrorNotificationsEnabled(false))
|
||||
Logger.showToast(this, this.getString(R.string.error_display_over_other_apps_permission_not_granted_to_start_terminal), true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -657,35 +733,35 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||
|
||||
/** If {@link TermuxActivity} has not bound to the {@link TermuxService} yet or is destroyed, then
|
||||
* interface functions requiring the activity should not be available to the terminal sessions,
|
||||
* so we just return the {@link #mTermuxTerminalSessionClientBase}. Once {@link TermuxActivity} bind
|
||||
* so we just return the {@link #mTermuxTerminalSessionServiceClient}. Once {@link TermuxActivity} bind
|
||||
* callback is received, it should call {@link #setTermuxTerminalSessionClient} to set the
|
||||
* {@link TermuxService#mTermuxTerminalSessionClient} so that further terminal sessions are directly
|
||||
* passed the {@link TermuxTerminalSessionClient} object which fully implements the
|
||||
* {@link TermuxService#mTermuxTerminalSessionActivityClient} so that further terminal sessions are directly
|
||||
* passed the {@link TermuxTerminalSessionActivityClient} object which fully implements the
|
||||
* {@link TerminalSessionClient} interface.
|
||||
*
|
||||
* @return Returns the {@link TermuxTerminalSessionClient} if {@link TermuxActivity} has bound with
|
||||
* {@link TermuxService}, otherwise {@link TermuxTerminalSessionClientBase}.
|
||||
* @return Returns the {@link TermuxTerminalSessionActivityClient} if {@link TermuxActivity} has bound with
|
||||
* {@link TermuxService}, otherwise {@link TermuxTerminalSessionServiceClient}.
|
||||
*/
|
||||
public synchronized TermuxTerminalSessionClientBase getTermuxTerminalSessionClient() {
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
return mTermuxTerminalSessionClient;
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
return mTermuxTerminalSessionActivityClient;
|
||||
else
|
||||
return mTermuxTerminalSessionClientBase;
|
||||
return mTermuxTerminalSessionServiceClient;
|
||||
}
|
||||
|
||||
/** This should be called when {@link TermuxActivity#onServiceConnected} is called to set the
|
||||
* {@link TermuxService#mTermuxTerminalSessionClient} variable and update the {@link TerminalSession}
|
||||
* and {@link TerminalEmulator} clients in case they were passed {@link TermuxTerminalSessionClientBase}
|
||||
* {@link TermuxService#mTermuxTerminalSessionActivityClient} variable and update the {@link TerminalSession}
|
||||
* and {@link TerminalEmulator} clients in case they were passed {@link TermuxTerminalSessionServiceClient}
|
||||
* earlier.
|
||||
*
|
||||
* @param termuxTerminalSessionClient The {@link TermuxTerminalSessionClient} object that fully
|
||||
* @param termuxTerminalSessionActivityClient The {@link TermuxTerminalSessionActivityClient} object that fully
|
||||
* implements the {@link TerminalSessionClient} interface.
|
||||
*/
|
||||
public synchronized void setTermuxTerminalSessionClient(TermuxTerminalSessionClient termuxTerminalSessionClient) {
|
||||
mTermuxTerminalSessionClient = termuxTerminalSessionClient;
|
||||
public synchronized void setTermuxTerminalSessionClient(TermuxTerminalSessionActivityClient termuxTerminalSessionActivityClient) {
|
||||
mTermuxTerminalSessionActivityClient = termuxTerminalSessionActivityClient;
|
||||
|
||||
for (int i = 0; i < mTermuxSessions.size(); i++)
|
||||
mTermuxSessions.get(i).getTerminalSession().updateTerminalSessionClient(mTermuxTerminalSessionClient);
|
||||
for (int i = 0; i < mShellManager.mTermuxSessions.size(); i++)
|
||||
mShellManager.mTermuxSessions.get(i).getTerminalSession().updateTerminalSessionClient(mTermuxTerminalSessionActivityClient);
|
||||
}
|
||||
|
||||
/** This should be called when {@link TermuxActivity} has been destroyed and in {@link #onUnbind(Intent)}
|
||||
|
|
@ -693,10 +769,10 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||
* clients do not hold an activity references.
|
||||
*/
|
||||
public synchronized void unsetTermuxTerminalSessionClient() {
|
||||
for (int i = 0; i < mTermuxSessions.size(); i++)
|
||||
mTermuxSessions.get(i).getTerminalSession().updateTerminalSessionClient(mTermuxTerminalSessionClientBase);
|
||||
for (int i = 0; i < mShellManager.mTermuxSessions.size(); i++)
|
||||
mShellManager.mTermuxSessions.get(i).getTerminalSession().updateTerminalSessionClient(mTermuxTerminalSessionServiceClient);
|
||||
|
||||
mTermuxTerminalSessionClient = null;
|
||||
mTermuxTerminalSessionActivityClient = null;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -713,7 +789,7 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||
|
||||
// Set notification text
|
||||
int sessionCount = getTermuxSessionsSize();
|
||||
int taskCount = mTermuxTasks.size();
|
||||
int taskCount = mShellManager.mTermuxTasks.size();
|
||||
String notificationText = sessionCount + " session" + (sessionCount == 1 ? "" : "s");
|
||||
if (taskCount > 0) {
|
||||
notificationText += ", " + taskCount + " task" + (taskCount == 1 ? "" : "s");
|
||||
|
|
@ -774,7 +850,7 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||
|
||||
/** Update the shown foreground service notification after making any changes that affect it. */
|
||||
private synchronized void updateNotification() {
|
||||
if (mWakeLock == null && mTermuxSessions.isEmpty() && mTermuxTasks.isEmpty()) {
|
||||
if (mWakeLock == null && mShellManager.mTermuxSessions.isEmpty() && mShellManager.mTermuxTasks.isEmpty()) {
|
||||
// Exit if we are updating after the user disabled all locks with no sessions or tasks running.
|
||||
requestStopService();
|
||||
} else {
|
||||
|
|
@ -786,41 +862,55 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||
|
||||
|
||||
|
||||
private void setCurrentStoredTerminalSession(TerminalSession session) {
|
||||
if (session == null) return;
|
||||
private void setCurrentStoredTerminalSession(TerminalSession terminalSession) {
|
||||
if (terminalSession == null) return;
|
||||
// Make the newly created session the current one to be displayed
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(this);
|
||||
if (preferences == null) return;
|
||||
preferences.setCurrentSession(session.mHandle);
|
||||
preferences.setCurrentSession(terminalSession.mHandle);
|
||||
}
|
||||
|
||||
public synchronized boolean isTermuxSessionsEmpty() {
|
||||
return mTermuxSessions.isEmpty();
|
||||
return mShellManager.mTermuxSessions.isEmpty();
|
||||
}
|
||||
|
||||
public synchronized int getTermuxSessionsSize() {
|
||||
return mTermuxSessions.size();
|
||||
return mShellManager.mTermuxSessions.size();
|
||||
}
|
||||
|
||||
public synchronized List<TermuxSession> getTermuxSessions() {
|
||||
return mTermuxSessions;
|
||||
return mShellManager.mTermuxSessions;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public synchronized TermuxSession getTermuxSession(int index) {
|
||||
if (index >= 0 && index < mTermuxSessions.size())
|
||||
return mTermuxSessions.get(index);
|
||||
if (index >= 0 && index < mShellManager.mTermuxSessions.size())
|
||||
return mShellManager.mTermuxSessions.get(index);
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public synchronized TermuxSession getTermuxSessionForTerminalSession(TerminalSession terminalSession) {
|
||||
if (terminalSession == null) return null;
|
||||
|
||||
for (int i = 0; i < mShellManager.mTermuxSessions.size(); i++) {
|
||||
if (mShellManager.mTermuxSessions.get(i).getTerminalSession().equals(terminalSession))
|
||||
return mShellManager.mTermuxSessions.get(i);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public synchronized TermuxSession getLastTermuxSession() {
|
||||
return mTermuxSessions.isEmpty() ? null : mTermuxSessions.get(mTermuxSessions.size() - 1);
|
||||
return mShellManager.mTermuxSessions.isEmpty() ? null : mShellManager.mTermuxSessions.get(mShellManager.mTermuxSessions.size() - 1);
|
||||
}
|
||||
|
||||
public synchronized int getIndexOfSession(TerminalSession terminalSession) {
|
||||
for (int i = 0; i < mTermuxSessions.size(); i++) {
|
||||
if (mTermuxSessions.get(i).getTerminalSession().equals(terminalSession))
|
||||
if (terminalSession == null) return -1;
|
||||
|
||||
for (int i = 0; i < mShellManager.mTermuxSessions.size(); i++) {
|
||||
if (mShellManager.mTermuxSessions.get(i).getTerminalSession().equals(terminalSession))
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
|
|
@ -828,20 +918,40 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||
|
||||
public synchronized TerminalSession getTerminalSessionForHandle(String sessionHandle) {
|
||||
TerminalSession terminalSession;
|
||||
for (int i = 0, len = mTermuxSessions.size(); i < len; i++) {
|
||||
terminalSession = mTermuxSessions.get(i).getTerminalSession();
|
||||
for (int i = 0, len = mShellManager.mTermuxSessions.size(); i < len; i++) {
|
||||
terminalSession = mShellManager.mTermuxSessions.get(i).getTerminalSession();
|
||||
if (terminalSession.mHandle.equals(sessionHandle))
|
||||
return terminalSession;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static synchronized int getNextExecutionId() {
|
||||
return EXECUTION_ID++;
|
||||
public synchronized AppShell getTermuxTaskForShellName(String name) {
|
||||
if (DataUtils.isNullOrEmpty(name)) return null;
|
||||
AppShell appShell;
|
||||
for (int i = 0, len = mShellManager.mTermuxTasks.size(); i < len; i++) {
|
||||
appShell = mShellManager.mTermuxTasks.get(i);
|
||||
String shellName = appShell.getExecutionCommand().shellName;
|
||||
if (shellName != null && shellName.equals(name))
|
||||
return appShell;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public synchronized TermuxSession getTermuxSessionForShellName(String name) {
|
||||
if (DataUtils.isNullOrEmpty(name)) return null;
|
||||
TermuxSession termuxSession;
|
||||
for (int i = 0, len = mShellManager.mTermuxSessions.size(); i < len; i++) {
|
||||
termuxSession = mShellManager.mTermuxSessions.get(i);
|
||||
String shellName = termuxSession.getExecutionCommand().shellName;
|
||||
if (shellName != null && shellName.equals(name))
|
||||
return termuxSession;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public boolean wantsToStop() {
|
||||
return mWantsToStop;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.termux.app.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
|
@ -12,10 +11,12 @@ import android.webkit.WebViewClient;
|
|||
import android.widget.ProgressBar;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
/** Basic embedded browser for viewing help pages. */
|
||||
public final class HelpActivity extends Activity {
|
||||
public final class HelpActivity extends AppCompatActivity {
|
||||
|
||||
WebView mWebView;
|
||||
|
||||
|
|
@ -34,7 +35,6 @@ public final class HelpActivity extends Activity {
|
|||
mWebView = new WebView(this);
|
||||
WebSettings settings = mWebView.getSettings();
|
||||
settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
|
||||
settings.setAppCacheEnabled(false);
|
||||
setContentView(progressLayout);
|
||||
mWebView.clearCache(true);
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import android.os.Bundle;
|
|||
import android.os.Environment;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
|
@ -16,20 +15,25 @@ import com.termux.shared.file.FileUtils;
|
|||
import com.termux.shared.models.ReportInfo;
|
||||
import com.termux.app.models.UserAction;
|
||||
import com.termux.shared.interact.ShareUtils;
|
||||
import com.termux.shared.packages.PackageUtils;
|
||||
import com.termux.shared.settings.preferences.TermuxAPIAppSharedPreferences;
|
||||
import com.termux.shared.settings.preferences.TermuxFloatAppSharedPreferences;
|
||||
import com.termux.shared.settings.preferences.TermuxTaskerAppSharedPreferences;
|
||||
import com.termux.shared.settings.preferences.TermuxWidgetAppSharedPreferences;
|
||||
import com.termux.shared.termux.AndroidUtils;
|
||||
import com.termux.shared.android.PackageUtils;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxAPIAppSharedPreferences;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxFloatAppSharedPreferences;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxTaskerAppSharedPreferences;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxWidgetAppSharedPreferences;
|
||||
import com.termux.shared.android.AndroidUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
import com.termux.shared.activity.media.AppCompatActivityUtils;
|
||||
import com.termux.shared.theme.NightMode;
|
||||
|
||||
public class SettingsActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
AppCompatActivityUtils.setNightMode(this, NightMode.getAppNightMode().getName(), true);
|
||||
|
||||
setContentView(R.layout.activity_settings);
|
||||
if (savedInstanceState == null) {
|
||||
getSupportFragmentManager()
|
||||
|
|
@ -37,11 +41,9 @@ public class SettingsActivity extends AppCompatActivity {
|
|||
.replace(R.id.settings, new RootPreferencesFragment())
|
||||
.commit();
|
||||
}
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
|
||||
AppCompatActivityUtils.setToolbar(this, com.termux.shared.R.id.toolbar);
|
||||
AppCompatActivityUtils.setShowBackButtonInActionBar(this, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -58,12 +60,17 @@ public class SettingsActivity extends AppCompatActivity {
|
|||
|
||||
setPreferencesFromResource(R.xml.root_preferences, rootKey);
|
||||
|
||||
configureTermuxAPIPreference(context);
|
||||
configureTermuxFloatPreference(context);
|
||||
configureTermuxTaskerPreference(context);
|
||||
configureTermuxWidgetPreference(context);
|
||||
configureAboutPreference(context);
|
||||
configureDonatePreference(context);
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
configureTermuxAPIPreference(context);
|
||||
configureTermuxFloatPreference(context);
|
||||
configureTermuxTaskerPreference(context);
|
||||
configureTermuxWidgetPreference(context);
|
||||
configureAboutPreference(context);
|
||||
configureDonatePreference(context);
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
private void configureTermuxAPIPreference(@NonNull Context context) {
|
||||
|
|
@ -112,22 +119,20 @@ public class SettingsActivity extends AppCompatActivity {
|
|||
String title = "About";
|
||||
|
||||
StringBuilder aboutString = new StringBuilder();
|
||||
aboutString.append(TermuxUtils.getAppInfoMarkdownString(context, false));
|
||||
|
||||
String termuxPluginAppsInfo = TermuxUtils.getTermuxPluginAppsInfoMarkdownString(context);
|
||||
if (termuxPluginAppsInfo != null)
|
||||
aboutString.append("\n\n").append(termuxPluginAppsInfo);
|
||||
|
||||
aboutString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
|
||||
aboutString.append(TermuxUtils.getAppInfoMarkdownString(context, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGES));
|
||||
aboutString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context, true));
|
||||
aboutString.append("\n\n").append(TermuxUtils.getImportantLinksMarkdownString(context));
|
||||
|
||||
String userActionName = UserAction.ABOUT.getName();
|
||||
ReportActivity.startReportActivity(context, new ReportInfo(userActionName,
|
||||
TermuxConstants.TERMUX_APP.TERMUX_SETTINGS_ACTIVITY_NAME, title, null,
|
||||
aboutString.toString(), null, false,
|
||||
userActionName,
|
||||
|
||||
ReportInfo reportInfo = new ReportInfo(userActionName,
|
||||
TermuxConstants.TERMUX_APP.TERMUX_SETTINGS_ACTIVITY_NAME, title);
|
||||
reportInfo.setReportString(aboutString.toString());
|
||||
reportInfo.setReportSaveFileLabelAndPath(userActionName,
|
||||
Environment.getExternalStorageDirectory() + "/" +
|
||||
FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true)));
|
||||
FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true));
|
||||
|
||||
ReportActivity.startReportActivity(context, reportInfo);
|
||||
}
|
||||
}.start();
|
||||
|
||||
|
|
@ -154,7 +159,7 @@ public class SettingsActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
donatePreference.setOnPreferenceClickListener(preference -> {
|
||||
ShareUtils.openURL(context, TermuxConstants.TERMUX_DONATE_URL);
|
||||
ShareUtils.openUrl(context, TermuxConstants.TERMUX_DONATE_URL);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,30 @@
|
|||
package com.termux.filepicker;
|
||||
package com.termux.app.api.file;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.util.Patterns;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.android.PackageUtils;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.data.IntentUtils;
|
||||
import com.termux.shared.net.uri.UriUtils;
|
||||
import com.termux.shared.interact.MessageDialogUtils;
|
||||
import com.termux.shared.interact.TextInputDialogUtils;
|
||||
import com.termux.shared.net.uri.UriScheme;
|
||||
import com.termux.shared.termux.interact.TextInputDialogUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||
import com.termux.app.TermuxService;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.termux.settings.properties.TermuxAppSharedProperties;
|
||||
import com.termux.shared.termux.settings.properties.TermuxPropertyConstants;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
|
|
@ -27,7 +36,7 @@ import java.io.InputStream;
|
|||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class TermuxFileReceiverActivity extends Activity {
|
||||
public class FileReceiverActivity extends AppCompatActivity {
|
||||
|
||||
static final String TERMUX_RECEIVEDIR = TermuxConstants.TERMUX_FILES_DIR_PATH + "/home/downloads";
|
||||
static final String EDITOR_PROGRAM = TermuxConstants.TERMUX_HOME_DIR_PATH + "/bin/termux-file-editor";
|
||||
|
|
@ -43,9 +52,11 @@ public class TermuxFileReceiverActivity extends Activity {
|
|||
|
||||
private static final String API_TAG = TermuxConstants.TERMUX_APP_NAME + "FileReceiver";
|
||||
|
||||
private static final String LOG_TAG = "TermuxFileReceiverActivity";
|
||||
private static final String LOG_TAG = "FileReceiverActivity";
|
||||
|
||||
static boolean isSharedTextAnUrl(String sharedText) {
|
||||
if (sharedText == null || sharedText.isEmpty()) return false;
|
||||
|
||||
return Patterns.WEB_URL.matcher(sharedText).matches()
|
||||
|| Pattern.matches("magnet:\\?xt=urn:btih:.*?", sharedText);
|
||||
}
|
||||
|
|
@ -89,11 +100,13 @@ public class TermuxFileReceiverActivity extends Activity {
|
|||
return;
|
||||
}
|
||||
|
||||
if ("content".equals(scheme)) {
|
||||
if (UriScheme.SCHEME_CONTENT.equals(scheme)) {
|
||||
handleContentUri(dataUri, sharedTitle);
|
||||
} else if ("file".equals(scheme)) {
|
||||
// When e.g. clicking on a downloaded apk:
|
||||
String path = dataUri.getPath();
|
||||
} else if (UriScheme.SCHEME_FILE.equals(scheme)) {
|
||||
Logger.logVerbose(LOG_TAG, "uri: \"" + dataUri + "\", path: \"" + dataUri.getPath() + "\", fragment: \"" + dataUri.getFragment() + "\"");
|
||||
|
||||
// Get full path including fragment (anything after last "#")
|
||||
String path = UriUtils.getUriFilePathWithFragment(dataUri);
|
||||
if (DataUtils.isNullOrEmpty(path)) {
|
||||
showErrorDialogAndQuit("File path from data uri is null, empty or invalid.");
|
||||
return;
|
||||
|
|
@ -121,8 +134,10 @@ public class TermuxFileReceiverActivity extends Activity {
|
|||
dialog -> finish());
|
||||
}
|
||||
|
||||
void handleContentUri(final Uri uri, String subjectFromIntent) {
|
||||
void handleContentUri(@NonNull final Uri uri, String subjectFromIntent) {
|
||||
try {
|
||||
Logger.logVerbose(LOG_TAG, "uri: \"" + uri + "\", path: \"" + uri.getPath() + "\", fragment: \"" + uri.getFragment() + "\"");
|
||||
|
||||
String attachmentFileName = null;
|
||||
|
||||
String[] projection = new String[]{OpenableColumns.DISPLAY_NAME};
|
||||
|
|
@ -134,6 +149,7 @@ public class TermuxFileReceiverActivity extends Activity {
|
|||
}
|
||||
|
||||
if (attachmentFileName == null) attachmentFileName = subjectFromIntent;
|
||||
if (attachmentFileName == null) attachmentFileName = UriUtils.getUriFileBasename(uri, true);
|
||||
|
||||
InputStream in = getContentResolver().openInputStream(uri);
|
||||
promptNameAndSave(in, attachmentFileName);
|
||||
|
|
@ -144,35 +160,36 @@ public class TermuxFileReceiverActivity extends Activity {
|
|||
}
|
||||
|
||||
void promptNameAndSave(final InputStream in, final String attachmentFileName) {
|
||||
TextInputDialogUtils.textInput(this, R.string.title_file_received, attachmentFileName, R.string.action_file_received_edit, text -> {
|
||||
File outFile = saveStreamWithName(in, text);
|
||||
if (outFile == null) return;
|
||||
TextInputDialogUtils.textInput(this, R.string.title_file_received, attachmentFileName,
|
||||
R.string.action_file_received_edit, text -> {
|
||||
File outFile = saveStreamWithName(in, text);
|
||||
if (outFile == null) return;
|
||||
|
||||
final File editorProgramFile = new File(EDITOR_PROGRAM);
|
||||
if (!editorProgramFile.isFile()) {
|
||||
showErrorDialogAndQuit("The following file does not exist:\n$HOME/bin/termux-file-editor\n\n"
|
||||
+ "Create this file as a script or a symlink - it will be called with the received file as only argument.");
|
||||
return;
|
||||
}
|
||||
final File editorProgramFile = new File(EDITOR_PROGRAM);
|
||||
if (!editorProgramFile.isFile()) {
|
||||
showErrorDialogAndQuit("The following file does not exist:\n$HOME/bin/termux-file-editor\n\n"
|
||||
+ "Create this file as a script or a symlink - it will be called with the received file as only argument.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Do this for the user if necessary:
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
editorProgramFile.setExecutable(true);
|
||||
// Do this for the user if necessary:
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
editorProgramFile.setExecutable(true);
|
||||
|
||||
final Uri scriptUri = new Uri.Builder().scheme("file").path(EDITOR_PROGRAM).build();
|
||||
final Uri scriptUri = UriUtils.getFileUri(EDITOR_PROGRAM);
|
||||
|
||||
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, scriptUri);
|
||||
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
||||
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()});
|
||||
startService(executeIntent);
|
||||
finish();
|
||||
},
|
||||
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, scriptUri);
|
||||
executeIntent.setClass(FileReceiverActivity.this, TermuxService.class);
|
||||
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()});
|
||||
startService(executeIntent);
|
||||
finish();
|
||||
},
|
||||
R.string.action_file_received_open_directory, text -> {
|
||||
if (saveStreamWithName(in, text) == null) return;
|
||||
|
||||
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE);
|
||||
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, TERMUX_RECEIVEDIR);
|
||||
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
||||
executeIntent.setClass(FileReceiverActivity.this, TermuxService.class);
|
||||
startService(executeIntent);
|
||||
finish();
|
||||
},
|
||||
|
|
@ -223,13 +240,48 @@ public class TermuxFileReceiverActivity extends Activity {
|
|||
//noinspection ResultOfMethodCallIgnored
|
||||
urlOpenerProgramFile.setExecutable(true);
|
||||
|
||||
final Uri urlOpenerProgramUri = new Uri.Builder().scheme("file").path(URL_OPENER_PROGRAM).build();
|
||||
final Uri urlOpenerProgramUri = UriUtils.getFileUri(URL_OPENER_PROGRAM);
|
||||
|
||||
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, urlOpenerProgramUri);
|
||||
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
||||
executeIntent.setClass(FileReceiverActivity.this, TermuxService.class);
|
||||
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, new String[]{url});
|
||||
startService(executeIntent);
|
||||
finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update {@link TERMUX_APP#FILE_SHARE_RECEIVER_ACTIVITY_CLASS_NAME} component state depending on
|
||||
* {@link TermuxPropertyConstants#KEY_DISABLE_FILE_SHARE_RECEIVER} value and
|
||||
* {@link TERMUX_APP#FILE_VIEW_RECEIVER_ACTIVITY_CLASS_NAME} component state depending on
|
||||
* {@link TermuxPropertyConstants#KEY_DISABLE_FILE_VIEW_RECEIVER} value.
|
||||
*/
|
||||
public static void updateFileReceiverActivityComponentsState(@NonNull Context context) {
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
TermuxAppSharedProperties properties = TermuxAppSharedProperties.getProperties();
|
||||
|
||||
String errmsg;
|
||||
boolean state;
|
||||
|
||||
state = !properties.isFileShareReceiverDisabled();
|
||||
Logger.logVerbose(LOG_TAG, "Setting " + TERMUX_APP.FILE_SHARE_RECEIVER_ACTIVITY_CLASS_NAME + " component state to " + state);
|
||||
errmsg = PackageUtils.setComponentState(context,TermuxConstants.TERMUX_PACKAGE_NAME,
|
||||
TERMUX_APP.FILE_SHARE_RECEIVER_ACTIVITY_CLASS_NAME,
|
||||
state, null, false, false);
|
||||
if (errmsg != null)
|
||||
Logger.logError(LOG_TAG, errmsg);
|
||||
|
||||
state = !properties.isFileViewReceiverDisabled();
|
||||
Logger.logVerbose(LOG_TAG, "Setting " + TERMUX_APP.FILE_VIEW_RECEIVER_ACTIVITY_CLASS_NAME + " component state to " + state);
|
||||
errmsg = PackageUtils.setComponentState(context,TermuxConstants.TERMUX_PACKAGE_NAME,
|
||||
TERMUX_APP.FILE_VIEW_RECEIVER_ACTIVITY_CLASS_NAME,
|
||||
state, null, false, false);
|
||||
if (errmsg != null)
|
||||
Logger.logError(LOG_TAG, errmsg);
|
||||
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
package com.termux.app.event;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.shared.data.IntentUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
import com.termux.shared.termux.file.TermuxFileUtils;
|
||||
import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment;
|
||||
import com.termux.shared.termux.shell.TermuxShellManager;
|
||||
|
||||
public class SystemEventReceiver extends BroadcastReceiver {
|
||||
|
||||
private static SystemEventReceiver mInstance;
|
||||
|
||||
private static final String LOG_TAG = "SystemEventReceiver";
|
||||
|
||||
public static synchronized SystemEventReceiver getInstance() {
|
||||
if (mInstance == null) {
|
||||
mInstance = new SystemEventReceiver();
|
||||
}
|
||||
return mInstance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(@NonNull Context context, @Nullable Intent intent) {
|
||||
if (intent == null) return;
|
||||
Logger.logDebug(LOG_TAG, "Intent Received:\n" + IntentUtils.getIntentString(intent));
|
||||
|
||||
String action = intent.getAction();
|
||||
if (action == null) return;
|
||||
|
||||
switch (action) {
|
||||
case Intent.ACTION_BOOT_COMPLETED:
|
||||
onActionBootCompleted(context, intent);
|
||||
break;
|
||||
case Intent.ACTION_PACKAGE_ADDED:
|
||||
case Intent.ACTION_PACKAGE_REMOVED:
|
||||
case Intent.ACTION_PACKAGE_REPLACED:
|
||||
onActionPackageUpdated(context, intent);
|
||||
break;
|
||||
default:
|
||||
Logger.logError(LOG_TAG, "Invalid action \"" + action + "\" passed to " + LOG_TAG);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void onActionBootCompleted(@NonNull Context context, @NonNull Intent intent) {
|
||||
TermuxShellManager.onActionBootCompleted(context, intent);
|
||||
}
|
||||
|
||||
public synchronized void onActionPackageUpdated(@NonNull Context context, @NonNull Intent intent) {
|
||||
Uri data = intent.getData();
|
||||
if (data != null && TermuxUtils.isUriDataForTermuxPluginPackage(data)) {
|
||||
Logger.logDebug(LOG_TAG, intent.getAction().replaceAll("^android.intent.action.", "") +
|
||||
" event received for \"" + data.toString().replaceAll("^package:", "") + "\"");
|
||||
if (TermuxFileUtils.isTermuxFilesDirectoryAccessible(context, false, false) == null)
|
||||
TermuxShellEnvironment.writeEnvironmentToFile(context);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Register {@link SystemEventReceiver} to listen to {@link Intent#ACTION_PACKAGE_ADDED},
|
||||
* {@link Intent#ACTION_PACKAGE_REMOVED} and {@link Intent#ACTION_PACKAGE_REPLACED} broadcasts.
|
||||
* They must be registered dynamically and cannot be registered implicitly in
|
||||
* the AndroidManifest.xml due to Android 8+ restrictions.
|
||||
*
|
||||
* https://developer.android.com/guide/components/broadcast-exceptions
|
||||
*/
|
||||
public synchronized static void registerPackageUpdateEvents(@NonNull Context context) {
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
|
||||
intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
|
||||
intentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED);
|
||||
intentFilter.addDataScheme("package");
|
||||
context.registerReceiver(getInstance(), intentFilter);
|
||||
}
|
||||
|
||||
public synchronized static void unregisterPackageUpdateEvents(@NonNull Context context) {
|
||||
context.unregisterReceiver(getInstance());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ import androidx.preference.PreferenceFragmentCompat;
|
|||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.settings.preferences.TermuxAPIAppSharedPreferences;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxAPIAppSharedPreferences;
|
||||
|
||||
@Keep
|
||||
public class TermuxAPIPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import androidx.preference.PreferenceFragmentCompat;
|
|||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.settings.preferences.TermuxFloatAppSharedPreferences;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxFloatAppSharedPreferences;
|
||||
|
||||
@Keep
|
||||
public class TermuxFloatPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import androidx.preference.PreferenceFragmentCompat;
|
|||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
|
||||
|
||||
@Keep
|
||||
public class TermuxPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import androidx.preference.PreferenceFragmentCompat;
|
|||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.settings.preferences.TermuxTaskerAppSharedPreferences;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxTaskerAppSharedPreferences;
|
||||
|
||||
@Keep
|
||||
public class TermuxTaskerPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import androidx.preference.PreferenceFragmentCompat;
|
|||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.settings.preferences.TermuxWidgetAppSharedPreferences;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxWidgetAppSharedPreferences;
|
||||
|
||||
@Keep
|
||||
public class TermuxWidgetPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import androidx.preference.PreferenceFragmentCompat;
|
|||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.logger.Logger;
|
||||
|
||||
@Keep
|
||||
|
|
@ -144,9 +144,9 @@ class DebuggingPreferencesDataStore extends PreferenceDataStore {
|
|||
case "terminal_view_key_logging_enabled":
|
||||
return mPreferences.isTerminalViewKeyLoggingEnabled();
|
||||
case "plugin_error_notifications_enabled":
|
||||
return mPreferences.arePluginErrorNotificationsEnabled();
|
||||
return mPreferences.arePluginErrorNotificationsEnabled(false);
|
||||
case "crash_report_notifications_enabled":
|
||||
return mPreferences.areCrashReportNotificationsEnabled();
|
||||
return mPreferences.areCrashReportNotificationsEnabled(false);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import androidx.preference.PreferenceFragmentCompat;
|
|||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
|
||||
|
||||
@Keep
|
||||
public class TerminalIOPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import androidx.preference.PreferenceFragmentCompat;
|
|||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
|
||||
|
||||
@Keep
|
||||
public class TerminalViewPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import androidx.preference.PreferenceFragmentCompat;
|
|||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.settings.preferences.TermuxAPIAppSharedPreferences;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxAPIAppSharedPreferences;
|
||||
|
||||
@Keep
|
||||
public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import androidx.preference.PreferenceFragmentCompat;
|
|||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.settings.preferences.TermuxFloatAppSharedPreferences;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxFloatAppSharedPreferences;
|
||||
|
||||
@Keep
|
||||
public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import androidx.preference.PreferenceFragmentCompat;
|
|||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.settings.preferences.TermuxTaskerAppSharedPreferences;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxTaskerAppSharedPreferences;
|
||||
|
||||
@Keep
|
||||
public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import androidx.preference.PreferenceFragmentCompat;
|
|||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.settings.preferences.TermuxWidgetAppSharedPreferences;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxWidgetAppSharedPreferences;
|
||||
|
||||
@Keep
|
||||
public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ package com.termux.app.models;
|
|||
public enum UserAction {
|
||||
|
||||
ABOUT("about"),
|
||||
CRASH_REPORT("crash report"),
|
||||
PLUGIN_EXECUTION_COMMAND("plugin execution command"),
|
||||
REPORT_ISSUE_FROM_TRANSCRIPT("report issue from transcript");
|
||||
|
||||
private final String name;
|
||||
|
|
|
|||
|
|
@ -1,119 +0,0 @@
|
|||
package com.termux.app.settings.properties;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.app.terminal.io.KeyboardShortcut;
|
||||
import com.termux.shared.terminal.io.extrakeys.ExtraKeysConstants;
|
||||
import com.termux.shared.terminal.io.extrakeys.ExtraKeysConstants.EXTRA_KEY_DISPLAY_MAPS;
|
||||
import com.termux.shared.terminal.io.extrakeys.ExtraKeysInfo;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.shared.settings.properties.TermuxSharedProperties;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class TermuxAppSharedProperties extends TermuxSharedProperties {
|
||||
|
||||
private ExtraKeysInfo mExtraKeysInfo;
|
||||
private List<KeyboardShortcut> mSessionShortcuts = new ArrayList<>();
|
||||
|
||||
private static final String LOG_TAG = "TermuxAppSharedProperties";
|
||||
|
||||
public TermuxAppSharedProperties(@NonNull Context context) {
|
||||
super(context, TermuxConstants.TERMUX_APP_NAME, TermuxPropertyConstants.getTermuxPropertiesFile(),
|
||||
TermuxPropertyConstants.TERMUX_PROPERTIES_LIST, new SharedPropertiesParserClient());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
ExtraKeysConstants.ExtraKeyDisplayMap extraKeyDisplayMap = ExtraKeysInfo.getCharDisplayMapForStyle(extraKeysStyle);
|
||||
if (EXTRA_KEY_DISPLAY_MAPS.DEFAULT_CHAR_DISPLAY.equals(extraKeyDisplayMap) && !TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE.equals(extraKeysStyle)) {
|
||||
Logger.logError(TermuxSharedProperties.LOG_TAG, "The style \"" + extraKeysStyle + "\" for the key \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE + "\" is invalid. Using default style instead.");
|
||||
extraKeysStyle = TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE;
|
||||
}
|
||||
|
||||
mExtraKeysInfo = new ExtraKeysInfo(extrakeys, extraKeysStyle, ExtraKeysConstants.CONTROL_CHARS_ALIASES);
|
||||
} catch (JSONException e) {
|
||||
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, ExtraKeysConstants.CONTROL_CHARS_ALIASES);
|
||||
} 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Load the {@link TermuxPropertyConstants#KEY_TERMINAL_TRANSCRIPT_ROWS} value from termux properties file on disk.
|
||||
*/
|
||||
public static int getTerminalTranscriptRows(Context context) {
|
||||
return (int) TermuxSharedProperties.getInternalPropertyValue(context, TermuxPropertyConstants.getTermuxPropertiesFile(),
|
||||
TermuxPropertyConstants.KEY_TERMINAL_TRANSCRIPT_ROWS, new SharedPropertiesParserClient());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -20,7 +20,9 @@ import androidx.core.content.ContextCompat;
|
|||
|
||||
import com.termux.R;
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.shared.shell.TermuxSession;
|
||||
import com.termux.shared.termux.shell.command.runner.terminal.TermuxSession;
|
||||
import com.termux.shared.theme.NightMode;
|
||||
import com.termux.shared.theme.ThemeUtils;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
import java.util.List;
|
||||
|
|
@ -55,9 +57,9 @@ public class TermuxSessionsListViewController extends ArrayAdapter<TermuxSession
|
|||
return sessionRowView;
|
||||
}
|
||||
|
||||
boolean isUsingBlackUI = mActivity.getProperties().isUsingBlackUI();
|
||||
boolean shouldEnableDarkTheme = ThemeUtils.shouldEnableDarkTheme(mActivity, NightMode.getAppNightMode().getName());
|
||||
|
||||
if (isUsingBlackUI) {
|
||||
if (shouldEnableDarkTheme) {
|
||||
sessionTitleView.setBackground(
|
||||
ContextCompat.getDrawable(mActivity, R.drawable.session_background_black_selected)
|
||||
);
|
||||
|
|
@ -84,7 +86,7 @@ public class TermuxSessionsListViewController extends ArrayAdapter<TermuxSession
|
|||
} else {
|
||||
sessionTitleView.setPaintFlags(sessionTitleView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
|
||||
}
|
||||
int defaultColor = isUsingBlackUI ? Color.WHITE : Color.BLACK;
|
||||
int defaultColor = shouldEnableDarkTheme ? Color.WHITE : Color.BLACK;
|
||||
int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? defaultColor : Color.RED;
|
||||
sessionTitleView.setTextColor(color);
|
||||
return sessionRowView;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.termux.app.terminal;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
|
|
@ -12,18 +13,23 @@ import android.media.SoundPool;
|
|||
import android.text.TextUtils;
|
||||
import android.widget.ListView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.shell.TermuxSession;
|
||||
import com.termux.shared.interact.TextInputDialogUtils;
|
||||
import com.termux.shared.interact.ShareUtils;
|
||||
import com.termux.shared.termux.shell.command.runner.terminal.TermuxSession;
|
||||
import com.termux.shared.termux.interact.TextInputDialogUtils;
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.shared.terminal.TermuxTerminalSessionClientBase;
|
||||
import com.termux.shared.termux.terminal.TermuxTerminalSessionClientBase;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.app.TermuxService;
|
||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.shared.terminal.io.BellHandler;
|
||||
import com.termux.shared.termux.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.shared.termux.terminal.io.BellHandler;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.terminal.TerminalColors;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
import com.termux.terminal.TerminalSessionClient;
|
||||
import com.termux.terminal.TextStyle;
|
||||
|
||||
import java.io.File;
|
||||
|
|
@ -31,7 +37,8 @@ import java.io.FileInputStream;
|
|||
import java.io.InputStream;
|
||||
import java.util.Properties;
|
||||
|
||||
public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase {
|
||||
/** The {@link TerminalSessionClient} implementation that may require an {@link Activity} for its interface methods. */
|
||||
public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionClientBase {
|
||||
|
||||
private final TermuxActivity mActivity;
|
||||
|
||||
|
|
@ -41,9 +48,9 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
|||
|
||||
private int mBellSoundId;
|
||||
|
||||
private static final String LOG_TAG = "TermuxTerminalSessionClient";
|
||||
private static final String LOG_TAG = "TermuxTerminalSessionActivityClient";
|
||||
|
||||
public TermuxTerminalSessionClient(TermuxActivity activity) {
|
||||
public TermuxTerminalSessionActivityClient(TermuxActivity activity) {
|
||||
this.mActivity = activity;
|
||||
}
|
||||
|
||||
|
|
@ -79,7 +86,7 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
|||
// Just initialize the mBellSoundPool and load the sound, otherwise bell might not run
|
||||
// the first time bell key is pressed and play() is called, since sound may not be loaded
|
||||
// quickly enough before the call to play(). https://stackoverflow.com/questions/35435625
|
||||
getBellSoundPool();
|
||||
loadBellSoundPool();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -100,7 +107,7 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
|||
/**
|
||||
* Should be called when mActivity.reloadActivityStyling() is called
|
||||
*/
|
||||
public void onReload() {
|
||||
public void onReloadActivityStyling() {
|
||||
// Set terminal fonts and colors
|
||||
checkForFontAndColors();
|
||||
}
|
||||
|
|
@ -108,14 +115,14 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
|||
|
||||
|
||||
@Override
|
||||
public void onTextChanged(TerminalSession changedSession) {
|
||||
public void onTextChanged(@NonNull TerminalSession changedSession) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
if (mActivity.getCurrentSession() == changedSession) mActivity.getTerminalView().onScreenUpdated();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTitleChanged(TerminalSession updatedSession) {
|
||||
public void onTitleChanged(@NonNull TerminalSession updatedSession) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
if (updatedSession != mActivity.getCurrentSession()) {
|
||||
|
|
@ -129,7 +136,7 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onSessionFinished(final TerminalSession finishedSession) {
|
||||
public void onSessionFinished(@NonNull TerminalSession finishedSession) {
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
|
||||
if (service == null || service.wantsToStop()) {
|
||||
|
|
@ -174,27 +181,23 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onCopyTextToClipboard(TerminalSession session, String text) {
|
||||
public void onCopyTextToClipboard(@NonNull TerminalSession session, String text) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(text)));
|
||||
ShareUtils.copyTextToClipboard(mActivity, text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPasteTextFromClipboard(TerminalSession session) {
|
||||
public void onPasteTextFromClipboard(@Nullable TerminalSession session) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clipData = clipboard.getPrimaryClip();
|
||||
if (clipData != null) {
|
||||
CharSequence paste = clipData.getItemAt(0).coerceToText(mActivity);
|
||||
if (!TextUtils.isEmpty(paste)) mActivity.getTerminalView().mEmulator.paste(paste.toString());
|
||||
}
|
||||
String text = ShareUtils.getTextStringFromClipboardIfSet(mActivity, true);
|
||||
if (text != null)
|
||||
mActivity.getTerminalView().mEmulator.paste(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBell(TerminalSession session) {
|
||||
public void onBell(@NonNull TerminalSession session) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
switch (mActivity.getProperties().getBellBehaviour()) {
|
||||
|
|
@ -202,7 +205,9 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
|||
BellHandler.getInstance(mActivity).doBell();
|
||||
break;
|
||||
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_BEEP:
|
||||
getBellSoundPool().play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
|
||||
loadBellSoundPool();
|
||||
if (mBellSoundPool != null)
|
||||
mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
|
||||
break;
|
||||
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_IGNORE:
|
||||
// Ignore the bell character.
|
||||
|
|
@ -211,7 +216,7 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onColorsChanged(TerminalSession changedSession) {
|
||||
public void onColorsChanged(@NonNull TerminalSession changedSession) {
|
||||
if (mActivity.getCurrentSession() == changedSession)
|
||||
updateBackgroundColor();
|
||||
}
|
||||
|
|
@ -229,6 +234,17 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
|||
mActivity.getTerminalView().setTerminalCursorBlinkerState(enabled, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTerminalShellPid(@NonNull TerminalSession terminalSession, int pid) {
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
if (service == null) return;
|
||||
|
||||
TermuxSession termuxSession = service.getTermuxSessionForTerminalSession(terminalSession);
|
||||
if (termuxSession != null)
|
||||
termuxSession.getExecutionCommand().mPid = pid;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Should be called when mActivity.onResetTerminalSession() is called
|
||||
*/
|
||||
|
|
@ -247,17 +263,20 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
|||
|
||||
|
||||
|
||||
/** Initialize and get mBellSoundPool */
|
||||
private synchronized SoundPool getBellSoundPool() {
|
||||
/** Load mBellSoundPool */
|
||||
private synchronized void loadBellSoundPool() {
|
||||
if (mBellSoundPool == null) {
|
||||
mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
|
||||
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
|
||||
|
||||
mBellSoundId = mBellSoundPool.load(mActivity, R.raw.bell, 1);
|
||||
try {
|
||||
mBellSoundId = mBellSoundPool.load(mActivity, com.termux.shared.R.raw.bell, 1);
|
||||
} catch (Exception e){
|
||||
// Catch java.lang.RuntimeException: Unable to resume activity {com.termux/com.termux.app.TermuxActivity}: android.content.res.Resources$NotFoundException: File res/raw/bell.ogg from drawable resource ID
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to load bell sound pool", e);
|
||||
}
|
||||
}
|
||||
|
||||
return mBellSoundPool;
|
||||
}
|
||||
|
||||
/** Release mBellSoundPool resources */
|
||||
|
|
@ -326,11 +345,22 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
|||
if (sessionToRename == null) return;
|
||||
|
||||
TextInputDialogUtils.textInput(mActivity, R.string.title_rename_session, sessionToRename.mSessionName, R.string.action_rename_session_confirm, text -> {
|
||||
sessionToRename.mSessionName = text;
|
||||
renameSession(sessionToRename, text);
|
||||
termuxSessionListNotifyUpdated();
|
||||
}, -1, null, -1, null, null);
|
||||
}
|
||||
|
||||
private void renameSession(TerminalSession sessionToRename, String text) {
|
||||
if (sessionToRename == null) return;
|
||||
sessionToRename.mSessionName = text;
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
if (service != null) {
|
||||
TermuxSession termuxSession = service.getTermuxSessionForTerminalSession(sessionToRename);
|
||||
if (termuxSession != null)
|
||||
termuxSession.getExecutionCommand().shellName = text;
|
||||
}
|
||||
}
|
||||
|
||||
public void addNewSession(boolean isFailSafe, String sessionName) {
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
if (service == null) return;
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.termux.app.terminal;
|
||||
|
||||
import android.app.Service;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.app.TermuxService;
|
||||
import com.termux.shared.termux.shell.command.runner.terminal.TermuxSession;
|
||||
import com.termux.shared.termux.terminal.TermuxTerminalSessionClientBase;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
import com.termux.terminal.TerminalSessionClient;
|
||||
|
||||
/** The {@link TerminalSessionClient} implementation that may require a {@link Service} for its interface methods. */
|
||||
public class TermuxTerminalSessionServiceClient extends TermuxTerminalSessionClientBase {
|
||||
|
||||
private static final String LOG_TAG = "TermuxTerminalSessionServiceClient";
|
||||
|
||||
private final TermuxService mService;
|
||||
|
||||
public TermuxTerminalSessionServiceClient(TermuxService service) {
|
||||
this.mService = service;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTerminalShellPid(@NonNull TerminalSession terminalSession, int pid) {
|
||||
TermuxSession termuxSession = mService.getTermuxSessionForTerminalSession(terminalSession);
|
||||
if (termuxSession != null)
|
||||
termuxSession.getExecutionCommand().mPid = pid;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -2,13 +2,10 @@ 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.os.Environment;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Gravity;
|
||||
|
|
@ -22,35 +19,37 @@ import android.widget.Toast;
|
|||
|
||||
import com.termux.R;
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.shared.data.UrlUtils;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.interact.MessageDialogUtils;
|
||||
import com.termux.shared.interact.ShareUtils;
|
||||
import com.termux.shared.shell.ShellUtils;
|
||||
import com.termux.shared.terminal.TermuxTerminalViewClientBase;
|
||||
import com.termux.shared.terminal.io.extrakeys.SpecialButton;
|
||||
import com.termux.shared.termux.AndroidUtils;
|
||||
import com.termux.shared.termux.TermuxBootstrap;
|
||||
import com.termux.shared.termux.terminal.TermuxTerminalViewClientBase;
|
||||
import com.termux.shared.termux.extrakeys.SpecialButton;
|
||||
import com.termux.shared.android.AndroidUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.activities.ReportActivity;
|
||||
import com.termux.shared.models.ReportInfo;
|
||||
import com.termux.app.models.UserAction;
|
||||
import com.termux.app.terminal.io.KeyboardShortcut;
|
||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.shared.termux.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.shared.termux.data.TermuxUrlUtils;
|
||||
import com.termux.shared.view.KeyboardUtils;
|
||||
import com.termux.shared.view.ViewUtils;
|
||||
import com.termux.terminal.KeyHandler;
|
||||
import com.termux.terminal.TerminalBuffer;
|
||||
import com.termux.terminal.TerminalEmulator;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
|
||||
|
|
@ -58,7 +57,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
|
||||
final TermuxActivity mActivity;
|
||||
|
||||
final TermuxTerminalSessionClient mTermuxTerminalSessionClient;
|
||||
final TermuxTerminalSessionActivityClient mTermuxTerminalSessionActivityClient;
|
||||
|
||||
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
|
||||
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
|
||||
|
|
@ -70,11 +69,13 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
|
||||
private boolean mTerminalCursorBlinkerStateAlreadySet;
|
||||
|
||||
private List<KeyboardShortcut> mSessionShortcuts;
|
||||
|
||||
private static final String LOG_TAG = "TermuxTerminalViewClient";
|
||||
|
||||
public TermuxTerminalViewClient(TermuxActivity activity, TermuxTerminalSessionClient termuxTerminalSessionClient) {
|
||||
public TermuxTerminalViewClient(TermuxActivity activity, TermuxTerminalSessionActivityClient termuxTerminalSessionActivityClient) {
|
||||
this.mActivity = activity;
|
||||
this.mTermuxTerminalSessionClient = termuxTerminalSessionClient;
|
||||
this.mTermuxTerminalSessionActivityClient = termuxTerminalSessionActivityClient;
|
||||
}
|
||||
|
||||
public TermuxActivity getActivity() {
|
||||
|
|
@ -85,6 +86,8 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
* Should be called when mActivity.onCreate() is called
|
||||
*/
|
||||
public void onCreate() {
|
||||
onReloadProperties();
|
||||
|
||||
mActivity.getTerminalView().setTextSize(mActivity.getPreferences().getFontSize());
|
||||
mActivity.getTerminalView().setKeepScreenOn(mActivity.getPreferences().shouldKeepScreenOn());
|
||||
}
|
||||
|
|
@ -108,7 +111,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
*/
|
||||
public void onResume() {
|
||||
// Show the soft keyboard if required
|
||||
setSoftKeyboardState(true, false);
|
||||
setSoftKeyboardState(true, mActivity.isActivityRecreated());
|
||||
|
||||
mTerminalCursorBlinkerStateAlreadySet = false;
|
||||
|
||||
|
|
@ -130,10 +133,17 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
setTerminalCursorBlinkerState(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when mActivity.reloadProperties() is called
|
||||
*/
|
||||
public void onReloadProperties() {
|
||||
setSessionShortcuts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when mActivity.reloadActivityStyling() is called
|
||||
*/
|
||||
public void onReload() {
|
||||
public void onReloadActivityStyling() {
|
||||
// Show the soft keyboard if required
|
||||
setSoftKeyboardState(false, true);
|
||||
|
||||
|
|
@ -179,11 +189,11 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
if (mActivity.getProperties().shouldOpenTerminalTranscriptURLOnClick()) {
|
||||
int[] columnAndRow = mActivity.getTerminalView().getColumnAndRow(e, true);
|
||||
String wordAtTap = term.getScreen().getWordAtLocation(columnAndRow[0], columnAndRow[1]);
|
||||
LinkedHashSet<CharSequence> urlSet = UrlUtils.extractUrls(wordAtTap);
|
||||
LinkedHashSet<CharSequence> urlSet = TermuxUrlUtils.extractUrls(wordAtTap);
|
||||
|
||||
if (!urlSet.isEmpty()) {
|
||||
String url = (String) urlSet.iterator().next();
|
||||
ShareUtils.openURL(mActivity, url);
|
||||
ShareUtils.openUrl(mActivity, url);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -213,7 +223,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
|
||||
@Override
|
||||
public boolean isTerminalViewSelected() {
|
||||
return mActivity.getTerminalToolbarViewPager() == null || mActivity.isTerminalViewSelected();
|
||||
return mActivity.getTerminalToolbarViewPager() == null || mActivity.isTerminalViewSelected() || mActivity.getTerminalView().hasFocus();
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -232,7 +242,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
if (handleVirtualKeys(keyCode, e, true)) return true;
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) {
|
||||
mTermuxTerminalSessionClient.removeFinishedSession(currentSession);
|
||||
mTermuxTerminalSessionActivityClient.removeFinishedSession(currentSession);
|
||||
return true;
|
||||
} else if (!mActivity.getProperties().areHardwareKeyboardShortcutsDisabled() &&
|
||||
e.isCtrlPressed() && e.isAltPressed()) {
|
||||
|
|
@ -240,9 +250,9 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
int unicodeChar = e.getUnicodeChar(0);
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || unicodeChar == 'n'/* next */) {
|
||||
mTermuxTerminalSessionClient.switchToSession(true);
|
||||
mTermuxTerminalSessionActivityClient.switchToSession(true);
|
||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_UP || unicodeChar == 'p' /* previous */) {
|
||||
mTermuxTerminalSessionClient.switchToSession(false);
|
||||
mTermuxTerminalSessionActivityClient.switchToSession(false);
|
||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
|
||||
mActivity.getDrawer().openDrawer(Gravity.LEFT);
|
||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
|
||||
|
|
@ -252,9 +262,9 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
} else if (unicodeChar == 'm'/* menu */) {
|
||||
mActivity.getTerminalView().showContextMenu();
|
||||
} else if (unicodeChar == 'r'/* rename */) {
|
||||
mTermuxTerminalSessionClient.renameSession(currentSession);
|
||||
mTermuxTerminalSessionActivityClient.renameSession(currentSession);
|
||||
} else if (unicodeChar == 'c'/* create */) {
|
||||
mTermuxTerminalSessionClient.addNewSession(false, null);
|
||||
mTermuxTerminalSessionActivityClient.addNewSession(false, null);
|
||||
} else if (unicodeChar == 'u' /* urls */) {
|
||||
showUrlSelection();
|
||||
} else if (unicodeChar == 'v') {
|
||||
|
|
@ -267,7 +277,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
changeFontSize(false);
|
||||
} else if (unicodeChar >= '1' && unicodeChar <= '9') {
|
||||
int index = unicodeChar - '1';
|
||||
mTermuxTerminalSessionClient.switchToSession(index);
|
||||
mTermuxTerminalSessionActivityClient.switchToSession(index);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -451,11 +461,11 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
return true;
|
||||
} else if (ctrlDown) {
|
||||
if (codePoint == 106 /* Ctrl+j or \n */ && !session.isRunning()) {
|
||||
mTermuxTerminalSessionClient.removeFinishedSession(session);
|
||||
mTermuxTerminalSessionActivityClient.removeFinishedSession(session);
|
||||
return true;
|
||||
}
|
||||
|
||||
List<KeyboardShortcut> shortcuts = mActivity.getProperties().getSessionShortcuts();
|
||||
List<KeyboardShortcut> shortcuts = mSessionShortcuts;
|
||||
if (shortcuts != null && !shortcuts.isEmpty()) {
|
||||
int codePointLowerCase = Character.toLowerCase(codePoint);
|
||||
for (int i = shortcuts.size() - 1; i >= 0; i--) {
|
||||
|
|
@ -463,16 +473,16 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
if (codePointLowerCase == shortcut.codePoint) {
|
||||
switch (shortcut.shortcutAction) {
|
||||
case TermuxPropertyConstants.ACTION_SHORTCUT_CREATE_SESSION:
|
||||
mTermuxTerminalSessionClient.addNewSession(false, null);
|
||||
mTermuxTerminalSessionActivityClient.addNewSession(false, null);
|
||||
return true;
|
||||
case TermuxPropertyConstants.ACTION_SHORTCUT_NEXT_SESSION:
|
||||
mTermuxTerminalSessionClient.switchToSession(true);
|
||||
mTermuxTerminalSessionActivityClient.switchToSession(true);
|
||||
return true;
|
||||
case TermuxPropertyConstants.ACTION_SHORTCUT_PREVIOUS_SESSION:
|
||||
mTermuxTerminalSessionClient.switchToSession(false);
|
||||
mTermuxTerminalSessionActivityClient.switchToSession(false);
|
||||
return true;
|
||||
case TermuxPropertyConstants.ACTION_SHORTCUT_RENAME_SESSION:
|
||||
mTermuxTerminalSessionClient.renameSession(mActivity.getCurrentSession());
|
||||
mTermuxTerminalSessionActivityClient.renameSession(mActivity.getCurrentSession());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -483,6 +493,27 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the terminal sessions shortcuts.
|
||||
*/
|
||||
private void setSessionShortcuts() {
|
||||
mSessionShortcuts = new ArrayList<>();
|
||||
|
||||
// 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) mActivity.getProperties().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 void changeFontSize(boolean increase) {
|
||||
|
|
@ -541,7 +572,8 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
// disabled or hidden at startup, otherwise if hardware keyboard is attached and user
|
||||
// starts typing on hardware keyboard without tapping on the terminal first, then a colour
|
||||
// tint will be added to the terminal as highlight for the focussed view. Test with a light
|
||||
// theme.
|
||||
// theme. For android 8.+, the "defaultFocusHighlightEnabled" attribute is also set to false
|
||||
// in TerminalView layout to fix the issue.
|
||||
|
||||
// If soft keyboard is disabled by user for Termux (check function docs for Termux behaviour info)
|
||||
if (KeyboardUtils.shouldSoftKeyboardBeDisabled(mActivity,
|
||||
|
|
@ -646,17 +678,17 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
|
||||
if (transcriptText == null) return;
|
||||
|
||||
try {
|
||||
// See https://github.com/termux/termux-app/issues/1166.
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
transcriptText = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
|
||||
intent.putExtra(Intent.EXTRA_TEXT, transcriptText);
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, mActivity.getString(R.string.title_share_transcript));
|
||||
mActivity.startActivity(Intent.createChooser(intent, mActivity.getString(R.string.title_share_transcript_with)));
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG,"Failed to get share session transcript of length " + transcriptText.length(), e);
|
||||
}
|
||||
// See https://github.com/termux/termux-app/issues/1166.
|
||||
transcriptText = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
|
||||
ShareUtils.shareText(mActivity, mActivity.getString(R.string.title_share_transcript),
|
||||
transcriptText, mActivity.getString(R.string.title_share_transcript_with));
|
||||
}
|
||||
|
||||
public void shareSelectedText() {
|
||||
String selectedText = mActivity.getTerminalView().getStoredSelectedText();
|
||||
if (DataUtils.isNullOrEmpty(selectedText)) return;
|
||||
ShareUtils.shareText(mActivity, mActivity.getString(R.string.title_share_selected_text),
|
||||
selectedText, mActivity.getString(R.string.title_share_selected_text_with));
|
||||
}
|
||||
|
||||
public void showUrlSelection() {
|
||||
|
|
@ -665,7 +697,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
|
||||
String text = ShellUtils.getTerminalSessionTranscriptText(session, true, true);
|
||||
|
||||
LinkedHashSet<CharSequence> urlSet = UrlUtils.extractUrls(text);
|
||||
LinkedHashSet<CharSequence> urlSet = TermuxUrlUtils.extractUrls(text);
|
||||
if (urlSet.isEmpty()) {
|
||||
new AlertDialog.Builder(mActivity).setMessage(R.string.title_select_url_none_found).show();
|
||||
return;
|
||||
|
|
@ -677,9 +709,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
// Click to copy url to clipboard:
|
||||
final AlertDialog dialog = new AlertDialog.Builder(mActivity).setItems(urls, (di, which) -> {
|
||||
String url = (String) urls[which];
|
||||
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(url)));
|
||||
Toast.makeText(mActivity, R.string.msg_select_url_copied_to_clipboard, Toast.LENGTH_LONG).show();
|
||||
ShareUtils.copyTextToClipboard(mActivity, url, mActivity.getString(R.string.msg_select_url_copied_to_clipboard));
|
||||
}).setTitle(R.string.title_select_url_dialog).create();
|
||||
|
||||
// Long press to open URL:
|
||||
|
|
@ -688,7 +718,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
lv.setOnItemLongClickListener((parent, view, position, id) -> {
|
||||
dialog.dismiss();
|
||||
String url = (String) urls[position];
|
||||
ShareUtils.openURL(mActivity, url);
|
||||
ShareUtils.openUrl(mActivity, url);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
|
@ -705,8 +735,8 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
|
||||
MessageDialogUtils.showMessage(mActivity, TermuxConstants.TERMUX_APP_NAME + " Report Issue",
|
||||
mActivity.getString(R.string.msg_add_termux_debug_info),
|
||||
mActivity.getString(R.string.action_yes), (dialog, which) -> reportIssueFromTranscript(transcriptText, true),
|
||||
mActivity.getString(R.string.action_no), (dialog, which) -> reportIssueFromTranscript(transcriptText, false),
|
||||
mActivity.getString(com.termux.shared.R.string.action_yes), (dialog, which) -> reportIssueFromTranscript(transcriptText, true),
|
||||
mActivity.getString(com.termux.shared.R.string.action_no), (dialog, which) -> reportIssueFromTranscript(transcriptText, false),
|
||||
null);
|
||||
}
|
||||
|
||||
|
|
@ -724,12 +754,19 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
reportString.append("\n").append(MarkdownUtils.getMarkdownCodeForString(transcriptText, true));
|
||||
reportString.append("\n##\n");
|
||||
|
||||
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, true));
|
||||
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(mActivity));
|
||||
if (addTermuxDebugInfo) {
|
||||
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGES));
|
||||
} else {
|
||||
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, TermuxUtils.AppInfoMode.TERMUX_PACKAGE));
|
||||
}
|
||||
|
||||
String termuxAptInfo = TermuxUtils.geAPTInfoMarkdownString(mActivity);
|
||||
if (termuxAptInfo != null)
|
||||
reportString.append("\n\n").append(termuxAptInfo);
|
||||
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(mActivity, true));
|
||||
|
||||
if (TermuxBootstrap.isAppPackageManagerAPT()) {
|
||||
String termuxAptInfo = TermuxUtils.geAPTInfoMarkdownString(mActivity);
|
||||
if (termuxAptInfo != null)
|
||||
reportString.append("\n\n").append(termuxAptInfo);
|
||||
}
|
||||
|
||||
if (addTermuxDebugInfo) {
|
||||
String termuxDebugInfo = TermuxUtils.getTermuxDebugMarkdownString(mActivity);
|
||||
|
|
@ -738,14 +775,16 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
}
|
||||
|
||||
String userActionName = UserAction.REPORT_ISSUE_FROM_TRANSCRIPT.getName();
|
||||
ReportActivity.startReportActivity(mActivity,
|
||||
new ReportInfo(userActionName,
|
||||
TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY_NAME, title, null,
|
||||
reportString.toString(), "\n\n" + TermuxUtils.getReportIssueMarkdownString(mActivity),
|
||||
false,
|
||||
userActionName,
|
||||
Environment.getExternalStorageDirectory() + "/" +
|
||||
FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true)));
|
||||
|
||||
ReportInfo reportInfo = new ReportInfo(userActionName,
|
||||
TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY_NAME, title);
|
||||
reportInfo.setReportString(reportString.toString());
|
||||
reportInfo.setReportStringSuffix("\n\n" + TermuxUtils.getReportIssueMarkdownString(mActivity));
|
||||
reportInfo.setReportSaveFileLabelAndPath(userActionName,
|
||||
Environment.getExternalStorageDirectory() + "/" +
|
||||
FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true));
|
||||
|
||||
ReportActivity.startReportActivity(mActivity, reportInfo);
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
|
@ -755,12 +794,9 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
if (session == null) return;
|
||||
if (!session.isRunning()) return;
|
||||
|
||||
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clipData = clipboard.getPrimaryClip();
|
||||
if (clipData == null) return;
|
||||
CharSequence paste = clipData.getItemAt(0).coerceToText(mActivity);
|
||||
if (!TextUtils.isEmpty(paste))
|
||||
session.getEmulator().paste(paste.toString());
|
||||
String text = ShareUtils.getTextStringFromClipboardIfSet(mActivity, true);
|
||||
if (text != null)
|
||||
session.getEmulator().paste(text);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import androidx.viewpager.widget.ViewPager;
|
|||
|
||||
import com.termux.R;
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.shared.terminal.io.extrakeys.ExtraKeysView;
|
||||
import com.termux.shared.termux.extrakeys.ExtraKeysView;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
public class TerminalToolbarViewPager {
|
||||
|
|
@ -44,11 +44,11 @@ public class TerminalToolbarViewPager {
|
|||
if (position == 0) {
|
||||
layout = inflater.inflate(R.layout.view_terminal_toolbar_extra_keys, collection, false);
|
||||
ExtraKeysView extraKeysView = (ExtraKeysView) layout;
|
||||
extraKeysView.setExtraKeysViewClient(new TermuxTerminalExtraKeys(mActivity.getTerminalView(),
|
||||
mActivity.getTermuxTerminalViewClient(), mActivity.getTermuxTerminalSessionClient()));
|
||||
extraKeysView.setExtraKeysViewClient(mActivity.getTermuxTerminalExtraKeys());
|
||||
extraKeysView.setButtonTextAllCaps(mActivity.getProperties().shouldExtraKeysTextBeAllCaps());
|
||||
mActivity.setExtraKeysView(extraKeysView);
|
||||
extraKeysView.reload(mActivity.getProperties().getExtraKeysInfo());
|
||||
extraKeysView.reload(mActivity.getTermuxTerminalExtraKeys().getExtraKeysInfo(),
|
||||
mActivity.getTerminalToolbarDefaultHeight());
|
||||
|
||||
// apply extra keys fix if enabled in prefs
|
||||
if (mActivity.getProperties().isUsingFullScreen() && mActivity.getProperties().isUsingFullScreenWorkAround()) {
|
||||
|
|
|
|||
|
|
@ -7,23 +7,78 @@ import android.view.View;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
|
||||
import com.termux.app.terminal.TermuxTerminalSessionClient;
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.app.terminal.TermuxTerminalSessionActivityClient;
|
||||
import com.termux.app.terminal.TermuxTerminalViewClient;
|
||||
import com.termux.shared.terminal.io.TerminalExtraKeys;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.termux.extrakeys.ExtraKeysConstants;
|
||||
import com.termux.shared.termux.extrakeys.ExtraKeysInfo;
|
||||
import com.termux.shared.termux.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.shared.termux.settings.properties.TermuxSharedProperties;
|
||||
import com.termux.shared.termux.terminal.io.TerminalExtraKeys;
|
||||
import com.termux.view.TerminalView;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
public class TermuxTerminalExtraKeys extends TerminalExtraKeys {
|
||||
|
||||
private ExtraKeysInfo mExtraKeysInfo;
|
||||
|
||||
TermuxTerminalViewClient mTermuxTerminalViewClient;
|
||||
TermuxTerminalSessionClient mTermuxTerminalSessionClient;
|
||||
final TermuxActivity mActivity;
|
||||
final TermuxTerminalViewClient mTermuxTerminalViewClient;
|
||||
final TermuxTerminalSessionActivityClient mTermuxTerminalSessionActivityClient;
|
||||
|
||||
public TermuxTerminalExtraKeys(@NonNull TerminalView terminalView,
|
||||
private static final String LOG_TAG = "TermuxTerminalExtraKeys";
|
||||
|
||||
public TermuxTerminalExtraKeys(TermuxActivity activity, @NonNull TerminalView terminalView,
|
||||
TermuxTerminalViewClient termuxTerminalViewClient,
|
||||
TermuxTerminalSessionClient termuxTerminalSessionClient) {
|
||||
TermuxTerminalSessionActivityClient termuxTerminalSessionActivityClient) {
|
||||
super(terminalView);
|
||||
|
||||
mActivity = activity;
|
||||
mTermuxTerminalViewClient = termuxTerminalViewClient;
|
||||
mTermuxTerminalSessionClient = termuxTerminalSessionClient;
|
||||
mTermuxTerminalSessionActivityClient = termuxTerminalSessionActivityClient;
|
||||
|
||||
setExtraKeys();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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) mActivity.getProperties().getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS, true);
|
||||
String extraKeysStyle = (String) mActivity.getProperties().getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE, true);
|
||||
|
||||
ExtraKeysConstants.ExtraKeyDisplayMap extraKeyDisplayMap = ExtraKeysInfo.getCharDisplayMapForStyle(extraKeysStyle);
|
||||
if (ExtraKeysConstants.EXTRA_KEY_DISPLAY_MAPS.DEFAULT_CHAR_DISPLAY.equals(extraKeyDisplayMap) && !TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE.equals(extraKeysStyle)) {
|
||||
Logger.logError(TermuxSharedProperties.LOG_TAG, "The style \"" + extraKeysStyle + "\" for the key \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE + "\" is invalid. Using default style instead.");
|
||||
extraKeysStyle = TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE;
|
||||
}
|
||||
|
||||
mExtraKeysInfo = new ExtraKeysInfo(extrakeys, extraKeysStyle, ExtraKeysConstants.CONTROL_CHARS_ALIASES);
|
||||
} catch (JSONException e) {
|
||||
Logger.showToast(mActivity, "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, ExtraKeysConstants.CONTROL_CHARS_ALIASES);
|
||||
} catch (JSONException e2) {
|
||||
Logger.showToast(mActivity, "Can't create default extra keys",true);
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Could create default extra keys: ", e);
|
||||
mExtraKeysInfo = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ExtraKeysInfo getExtraKeysInfo() {
|
||||
return mExtraKeysInfo;
|
||||
}
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
|
|
@ -39,8 +94,12 @@ public class TermuxTerminalExtraKeys extends TerminalExtraKeys {
|
|||
else
|
||||
drawerLayout.openDrawer(Gravity.LEFT);
|
||||
} else if ("PASTE".equals(key)) {
|
||||
if(mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.onPasteTextFromClipboard(null);
|
||||
if(mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.onPasteTextFromClipboard(null);
|
||||
} else if ("SCROLL".equals(key)) {
|
||||
TerminalView terminalView = mTermuxTerminalViewClient.getActivity().getTerminalView();
|
||||
if (terminalView != null && terminalView.mEmulator != null)
|
||||
terminalView.mEmulator.toggleAutoScrollDisabled();
|
||||
} else {
|
||||
super.onTerminalExtraKeyButtonClick(view, key, ctrlDown, altDown, shiftDown, fnDown);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,214 +0,0 @@
|
|||
package com.termux.app.utils;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.os.Environment;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.activities.ReportActivity;
|
||||
import com.termux.shared.models.errors.Error;
|
||||
import com.termux.shared.notification.NotificationUtils;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.models.ReportInfo;
|
||||
import com.termux.app.models.UserAction;
|
||||
import com.termux.shared.notification.TermuxNotificationUtils;
|
||||
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.AndroidUtils;
|
||||
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 an app crash at last run 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 notifyAppCrashOnLastRun(final Context context, final String logTagParam) {
|
||||
if (context == null) return;
|
||||
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
||||
if (preferences == null) return;
|
||||
|
||||
// If user has disabled notifications for crashes
|
||||
if (!preferences.areCrashReportNotificationsEnabled())
|
||||
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;
|
||||
|
||||
Error error;
|
||||
StringBuilder reportStringBuilder = new StringBuilder();
|
||||
|
||||
// Read report string from crash log file
|
||||
error = FileUtils.readStringFromFile("crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, Charset.defaultCharset(), reportStringBuilder, false);
|
||||
if (error != null) {
|
||||
Logger.logErrorExtended(logTag, error.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
// Move crash log file to backup location if it exists
|
||||
error = FileUtils.moveRegularFile("crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, TermuxConstants.TERMUX_CRASH_LOG_BACKUP_FILE_PATH, true);
|
||||
if (error != null) {
|
||||
Logger.logErrorExtended(logTag, error.toString());
|
||||
}
|
||||
|
||||
String reportString = reportStringBuilder.toString();
|
||||
|
||||
if (reportString.isEmpty())
|
||||
return;
|
||||
|
||||
Logger.logDebug(logTag, "A crash log file found at \"" + TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH + "\".");
|
||||
|
||||
sendCrashReportNotification(context, logTag, reportString, false, false);
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a crash report notification 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 logTag The log tag to use for logging.
|
||||
* @param message The message for the crash report.
|
||||
* @param forceNotification If set to {@code true}, then a notification will be shown
|
||||
* regardless of if pending intent is {@code null} or
|
||||
* {@link TermuxPreferenceConstants.TERMUX_APP#KEY_CRASH_REPORT_NOTIFICATIONS_ENABLED}
|
||||
* is {@code false}.
|
||||
* @param addAppAndDeviceInfo If set to {@code true}, then app and device info will be appended
|
||||
* to the message.
|
||||
*/
|
||||
public static void sendCrashReportNotification(final Context context, String logTag, String message, boolean forceNotification, boolean addAppAndDeviceInfo) {
|
||||
if (context == null) return;
|
||||
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
||||
if (preferences == null) return;
|
||||
|
||||
// If user has disabled notifications for crashes
|
||||
if (!preferences.areCrashReportNotificationsEnabled() && !forceNotification)
|
||||
return;
|
||||
|
||||
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||
|
||||
// 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, "Sending \"" + title + "\" notification.");
|
||||
|
||||
StringBuilder reportString = new StringBuilder(message);
|
||||
|
||||
if (addAppAndDeviceInfo) {
|
||||
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true));
|
||||
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
|
||||
}
|
||||
|
||||
String userActionName = UserAction.CRASH_REPORT.getName();
|
||||
ReportActivity.NewInstanceResult result = ReportActivity.newInstance(context, new ReportInfo(userActionName,
|
||||
logTag, title, null, reportString.toString(),
|
||||
"\n\n" + TermuxUtils.getReportIssueMarkdownString(context), true,
|
||||
userActionName,
|
||||
Environment.getExternalStorageDirectory() + "/" +
|
||||
FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true)));
|
||||
if (result.contentIntent == null) return;
|
||||
|
||||
// Must ensure result code for PendingIntents and id for notification are unique otherwise will override previous
|
||||
int nextNotificationId = TermuxNotificationUtils.getNextNotificationId(context);
|
||||
|
||||
PendingIntent contentIntent = PendingIntent.getActivity(context, nextNotificationId, result.contentIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
PendingIntent deleteIntent = null;
|
||||
if (result.deleteIntent != null)
|
||||
deleteIntent = PendingIntent.getBroadcast(context, nextNotificationId, result.deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
// Setup the notification channel if not already set up
|
||||
setupCrashReportsNotificationChannel(context);
|
||||
|
||||
// Build the notification
|
||||
Notification.Builder builder = getCrashReportsNotificationBuilder(context, title, null,
|
||||
null, contentIntent, deleteIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
|
||||
if (builder == null) return;
|
||||
|
||||
// Send the notification
|
||||
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
|
||||
if (notificationManager != null)
|
||||
notificationManager.notify(nextNotificationId, builder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 contentIntent The {@link PendingIntent} which should be sent when notification is clicked.
|
||||
* @param deleteIntent The {@link PendingIntent} which should be sent when notification is deleted.
|
||||
* @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}.
|
||||
* @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 contentIntent, final PendingIntent deleteIntent, final int notificationMode) {
|
||||
|
||||
Notification.Builder builder = NotificationUtils.geNotificationBuilder(context,
|
||||
TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_HIGH,
|
||||
title, notificationText, notificationBigText, contentIntent, deleteIntent, 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,9 +1,16 @@
|
|||
<?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:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include
|
||||
layout="@layout/partial_primary_toolbar"
|
||||
android:id="@+id/partial_primary_toolbar"/>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
<com.termux.app.terminal.TermuxActivityRootView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/activity_termux_root_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
|
@ -25,23 +27,23 @@
|
|||
android:id="@+id/terminal_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:defaultFocusHighlightEnabled="false"
|
||||
android:focusableInTouchMode="true"
|
||||
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
|
||||
android:scrollbars="vertical"
|
||||
android:importantForAutofill="no"
|
||||
android:autofillHints="password" />
|
||||
tools:ignore="UnusedAttribute" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/left_drawer"
|
||||
android:layout_width="240dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
android:background="@android:color/white"
|
||||
android:choiceMode="singleChoice"
|
||||
android:divider="@android:color/transparent"
|
||||
android:dividerHeight="0dp"
|
||||
android:descendantFocusability="blocksDescendants"
|
||||
android:orientation="vertical">
|
||||
android:orientation="vertical"
|
||||
android:background="?attr/termuxActivityDrawerBackground">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
|
|
@ -53,7 +55,8 @@
|
|||
android:layout_height="40dp"
|
||||
android:src="@drawable/ic_settings"
|
||||
android:background="@null"
|
||||
android:contentDescription="@string/action_open_settings" />
|
||||
android:contentDescription="@string/action_open_settings"
|
||||
app:tint="?attr/termuxActivityDrawerImageTint" />
|
||||
</LinearLayout>
|
||||
|
||||
<ListView
|
||||
|
|
@ -71,7 +74,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/toggle_keyboard_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="match_parent"
|
||||
|
|
@ -79,7 +82,7 @@
|
|||
android:layout_weight="1"
|
||||
android:text="@string/action_toggle_soft_keyboard" />
|
||||
|
||||
<Button
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/new_session_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="match_parent"
|
||||
|
|
@ -96,7 +99,7 @@
|
|||
android:visibility="gone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="37.5dp"
|
||||
android:background="@android:drawable/screen_background_dark_transparent"
|
||||
android:background="@color/black"
|
||||
android:layout_alignParentBottom="true" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<com.google.android.material.textview.MaterialTextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/session_title"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="?android:attr/listPreferredItemHeight"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.termux.shared.terminal.io.extrakeys.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<com.termux.shared.termux.extrakeys.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/terminal_toolbar_extra_keys"
|
||||
style="?android:attr/buttonBarStyle"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
|||
|
|
@ -2,4 +2,5 @@
|
|||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@android:color/black"/>
|
||||
<foreground android:drawable="@drawable/ic_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
|
|
|||
|
|
@ -2,4 +2,5 @@
|
|||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@android:color/black"/>
|
||||
<foreground android:drawable="@drawable/ic_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!--
|
||||
https://material.io/develop/android/theming/dark
|
||||
-->
|
||||
|
||||
<!-- TermuxActivity DayNight NoActionBar 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. -->
|
||||
<style name="Theme.TermuxActivity.DayNight.NoActionBar" parent="Theme.TermuxApp.DayNight.NoActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/black</item>
|
||||
<item name="colorPrimaryVariant">@color/black</item>
|
||||
|
||||
<item name="android:windowBackground">@color/black</item>
|
||||
|
||||
<!-- Avoid action mode toolbar pushing down terminal content when
|
||||
selecting text on pre-6.0 (non-floating toolbar). -->
|
||||
<item name="android:windowActionModeOverlay">true</item>
|
||||
|
||||
<item name="android:windowTranslucentStatus">true</item>
|
||||
<item name="android:windowTranslucentNavigation">true</item>
|
||||
|
||||
<!-- https://developer.android.com/training/tv/start/start.html#transition-color -->
|
||||
<item name="android:windowAllowReturnTransitionOverlap">true</item>
|
||||
<item name="android:windowAllowEnterTransitionOverlap">true</item>
|
||||
|
||||
<!-- Left drawer. -->
|
||||
<item name="buttonBarButtonStyle">@style/TermuxActivity.Drawer.ButtonBarStyle.Dark</item>
|
||||
<item name="termuxActivityDrawerBackground">@color/black</item>
|
||||
<item name="termuxActivityDrawerImageTint">@color/white</item>
|
||||
|
||||
<!-- Extra keys colors. -->
|
||||
<item name="extraKeysButtonTextColor">@color/white</item>
|
||||
<item name="extraKeysButtonActiveTextColor">@color/red_400</item>
|
||||
<item name="extraKeysButtonBackgroundColor">@color/black</item>
|
||||
<item name="extraKeysButtonActiveBackgroundColor">@color/grey_500</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<attr name="termuxActivityDrawerBackground" format="reference" />
|
||||
<attr name="termuxActivityDrawerImageTint" format="reference" />
|
||||
</resources>
|
||||
|
|
@ -33,7 +33,11 @@
|
|||
<string name="bootstrap_error_try_again">Try again</string>
|
||||
<string name="bootstrap_error_not_primary_user_message">&TERMUX_APP_NAME; can only be run as the primary user.
|
||||
\nBootstrap binaries compiled for &TERMUX_APP_NAME; have hardcoded $PREFIX path and cannot be installed
|
||||
under any path other than %1$s.</string>
|
||||
under any path other than:\n%1$s.</string>
|
||||
<string name="bootstrap_error_installed_on_portable_sd">&TERMUX_APP_NAME; cannot be installed on
|
||||
portable/external/removable sd card on your device.
|
||||
\nBootstrap binaries compiled for &TERMUX_APP_NAME; have hardcoded $PREFIX path and cannot be installed
|
||||
under any path other than:\n%1$s.</string>
|
||||
|
||||
|
||||
|
||||
|
|
@ -65,6 +69,11 @@
|
|||
<string name="title_share_transcript">Terminal transcript</string>
|
||||
<string name="title_share_transcript_with">Send transcript to:</string>
|
||||
|
||||
<string name="action_share_selected_text">Share selected text</string>
|
||||
<string name="title_share_selected_text">Terminal Text</string>
|
||||
<string name="title_share_selected_text_with">Send selected text to:</string>
|
||||
|
||||
<string name="action_autofill_username">Autofill username</string>
|
||||
<string name="action_autofill_password">Autofill password</string>
|
||||
|
||||
<string name="action_reset_terminal">Reset</string>
|
||||
|
|
@ -95,14 +104,19 @@
|
|||
|
||||
|
||||
<!-- TermuxService -->
|
||||
<string name="error_display_over_other_apps_permission_not_granted">&TERMUX_APP_NAME; requires
|
||||
<string name="error_display_over_other_apps_permission_not_granted_to_start_terminal">&TERMUX_APP_NAME; requires
|
||||
\"Display over other apps\" permission to start terminal sessions from background on Android >= 10.
|
||||
Grants it from Settings -> Apps -> &TERMUX_APP_NAME; -> Advanced</string>
|
||||
<string name="error_termux_service_invalid_execution_command_runner">Invalid execution command runner to TermuxService: `%1$s`</string>
|
||||
<string name="error_termux_service_unsupported_execution_command_runner">Unsupported execution command runner to TermuxService: `%1$s`</string>
|
||||
<string name="error_termux_service_unsupported_execution_command_shell_create_mode">Unsupported execution command shell create mode to TermuxService: `%1$s`</string>
|
||||
<string name="error_termux_service_execution_command_shell_name_unset">Shell name not set but `%1$s` shell create mode passed</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_invalid_execution_command_runner">Invalid execution command runner 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_api_help">Visit %1$s for more info on RUN_COMMAND Intent usage.</string>
|
||||
|
||||
|
|
@ -116,8 +130,8 @@
|
|||
|
||||
|
||||
<!-- Miscellaneous -->
|
||||
<string name="error_allow_external_apps_ungranted">%1$s requires `allow-external-apps`
|
||||
property to be set to `true` in `%2$s` file.</string>
|
||||
<string name="error_termux_service_start_failed_general">Failed to start TermuxService. Check logcat for exception message.</string>
|
||||
<string name="error_termux_service_start_failed_bg">Failed to start TermuxService while app is in background due to android bg restrictions.</string>
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,52 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<style name="Theme.Termux" parent="@android:style/Theme.Material.Light.NoActionBar">
|
||||
<item name="android:statusBarColor">#000000</item>
|
||||
<item name="android:colorPrimary">#FF000000</item>
|
||||
<item name="android:windowBackground">@android:color/black</item>
|
||||
|
||||
<!-- Seen in buttons on left drawer: -->
|
||||
<item name="android:colorAccent">#212121</item>
|
||||
<item name="android:alertDialogTheme">@style/TermuxAlertDialogStyle</item>
|
||||
<!-- Avoid action mode toolbar pushing down terminal content when
|
||||
selecting text on pre-6.0 (non-floating toolbar). -->
|
||||
<item name="android:windowActionModeOverlay">true</item>
|
||||
|
||||
<item name="android:windowTranslucentStatus">true</item>
|
||||
<item name="android:windowTranslucentNavigation">true</item>
|
||||
|
||||
<!-- https://developer.android.com/training/tv/start/start.html#transition-color -->
|
||||
<item name="android:windowAllowReturnTransitionOverlap">true</item>
|
||||
<item name="android:windowAllowEnterTransitionOverlap">true</item>
|
||||
</style>
|
||||
|
||||
|
||||
<!-- See https://developer.android.com/training/material/theme.html for how to customize the Material theme. -->
|
||||
<!-- NOTE: Cannot use "Light." since it hides the terminal scrollbar on the default black background. -->
|
||||
<style name="Theme.Termux.Black" parent="@android:style/Theme.Material.NoActionBar">
|
||||
<item name="android:statusBarColor">#000000</item>
|
||||
<item name="android:colorPrimary">#FF000000</item>
|
||||
<item name="android:windowBackground">@android:color/black</item>
|
||||
|
||||
<!-- Seen in buttons on left drawer: -->
|
||||
<item name="android:colorAccent">#FDFDFD</item>
|
||||
<!-- Avoid action mode toolbar pushing down terminal content when
|
||||
selecting text on pre-6.0 (non-floating toolbar). -->
|
||||
<item name="android:windowActionModeOverlay">true</item>
|
||||
|
||||
<item name="android:windowTranslucentStatus">true</item>
|
||||
<item name="android:windowTranslucentNavigation">true</item>
|
||||
|
||||
<!-- https://developer.android.com/training/tv/start/start.html#transition-color -->
|
||||
<item name="android:windowAllowReturnTransitionOverlap">true</item>
|
||||
<item name="android:windowAllowEnterTransitionOverlap">true</item>
|
||||
</style>
|
||||
|
||||
|
||||
<style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">
|
||||
<!-- Seen in buttons on alert dialog: -->
|
||||
<item name="android:colorAccent">#212121</item>
|
||||
</style>
|
||||
|
||||
<style name="TermuxActivity.Drawer.ButtonBarStyle.Light" parent="@style/Widget.MaterialComponents.Button.TextButton">
|
||||
<item name="android:textColor">@color/black</item>
|
||||
</style>
|
||||
|
||||
<style name="TermuxActivity.Drawer.ButtonBarStyle.Dark" parent="@style/Widget.MaterialComponents.Button.TextButton">
|
||||
<item name="android:textColor">@color/white</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!--
|
||||
https://material.io/develop/android/theming/dark
|
||||
-->
|
||||
|
||||
<!-- TermuxApp Light DarkActionBar theme. -->
|
||||
<style name="Theme.TermuxApp.Light.DarkActionBar" parent="Theme.BaseActivity.Light.DarkActionBar"/>
|
||||
<!-- TermuxApp Light NoActionBar theme. -->
|
||||
<style name="Theme.TermuxApp.Light.NoActionBar" parent="Theme.BaseActivity.Light.NoActionBar"/>
|
||||
|
||||
<!-- TermuxApp DayNight DarkActionBar theme. -->
|
||||
<style name="Theme.TermuxApp.DayNight.DarkActionBar" parent="Theme.BaseActivity.DayNight.DarkActionBar"/>
|
||||
<!-- TermuxApp DayNight NoActionBar theme. -->
|
||||
<style name="Theme.TermuxApp.DayNight.NoActionBar" parent="Theme.BaseActivity.DayNight.NoActionBar"/>
|
||||
|
||||
|
||||
<!-- TermuxActivity DayNight NoActionBar theme. -->
|
||||
<style name="Theme.TermuxActivity.DayNight.NoActionBar" parent="Theme.TermuxApp.DayNight.NoActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/black</item>
|
||||
<item name="colorPrimaryVariant">@color/black</item>
|
||||
|
||||
<item name="android:windowBackground">@color/black</item>
|
||||
|
||||
<!-- Avoid action mode toolbar pushing down terminal content when
|
||||
selecting text on pre-6.0 (non-floating toolbar). -->
|
||||
<item name="android:windowActionModeOverlay">true</item>
|
||||
|
||||
<item name="android:windowTranslucentStatus">true</item>
|
||||
<item name="android:windowTranslucentNavigation">true</item>
|
||||
|
||||
<!-- https://developer.android.com/training/tv/start/start.html#transition-color -->
|
||||
<item name="android:windowAllowReturnTransitionOverlap">true</item>
|
||||
<item name="android:windowAllowEnterTransitionOverlap">true</item>
|
||||
|
||||
<!-- Left drawer. -->
|
||||
<item name="buttonBarButtonStyle">@style/TermuxActivity.Drawer.ButtonBarStyle.Light</item>
|
||||
<item name="termuxActivityDrawerBackground">@color/white</item>
|
||||
<item name="termuxActivityDrawerImageTint">@color/black</item>
|
||||
|
||||
<!-- Extra keys colors. -->
|
||||
<item name="extraKeysButtonTextColor">@color/white</item>
|
||||
<item name="extraKeysButtonActiveTextColor">@color/red_400</item>
|
||||
<item name="extraKeysButtonBackgroundColor">@color/black</item>
|
||||
<item name="extraKeysButtonActiveBackgroundColor">@color/grey_500</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.termux.app;
|
||||
|
||||
import com.termux.shared.data.UrlUtils;
|
||||
import com.termux.shared.termux.data.TermuxUrlUtils;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
|
@ -13,7 +13,7 @@ public class TermuxActivityTest {
|
|||
private void assertUrlsAre(String text, String... urls) {
|
||||
LinkedHashSet<String> expected = new LinkedHashSet<>();
|
||||
Collections.addAll(expected, urls);
|
||||
Assert.assertEquals(expected, UrlUtils.extractUrls(text));
|
||||
Assert.assertEquals(expected, TermuxUrlUtils.extractUrls(text));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
package com.termux.filepicker;
|
||||
package com.termux.app.api.file;
|
||||
|
||||
import com.termux.app.api.file.FileReceiverActivity;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
|
@ -9,7 +11,7 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class TermuxFileReceiverActivityTest {
|
||||
public class FileReceiverActivityTest {
|
||||
|
||||
@Test
|
||||
public void testIsSharedTextAnUrl() {
|
||||
|
|
@ -19,13 +21,15 @@ public class TermuxFileReceiverActivityTest {
|
|||
validUrls.add("https://example.com/path/parameter=foo");
|
||||
validUrls.add("magnet:?xt=urn:btih:d540fc48eb12f2833163eed6421d449dd8f1ce1f&dn=Ubuntu+desktop+19.04+%2864bit%29&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=udp%3A%2F%2Ftracker.publicbt.com%3A80&tr=udp%3A%2F%2Ftracker.ccc.de%3A80");
|
||||
for (String url : validUrls) {
|
||||
Assert.assertTrue(TermuxFileReceiverActivity.isSharedTextAnUrl(url));
|
||||
Assert.assertTrue(FileReceiverActivity.isSharedTextAnUrl(url));
|
||||
}
|
||||
|
||||
List<String> invalidUrls = new ArrayList<>();
|
||||
invalidUrls.add("a test with example.com");
|
||||
invalidUrls.add("");
|
||||
invalidUrls.add(null);
|
||||
for (String url : invalidUrls) {
|
||||
Assert.assertFalse(TermuxFileReceiverActivity.isSharedTextAnUrl(url));
|
||||
Assert.assertFalse(FileReceiverActivity.isSharedTextAnUrl(url));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4,7 +4,7 @@ buildscript {
|
|||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.2.2'
|
||||
classpath "com.android.tools.build:gradle:8.13.2"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -15,7 +15,3 @@ allprojects {
|
|||
maven { url "https://jitpack.io" }
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
page_ref: /docs/apps/termux/index.html
|
||||
---
|
||||
|
||||
# Termux App Docs
|
||||
|
||||
<!--- DOC_HEADER_PLACEHOLDER -->
|
||||
|
||||
Welcome to documentation for the [Termux App].
|
||||
|
||||
##
|
||||
|
||||
[Termux App]: https://github.com/termux/termux-app
|
||||
|
|
@ -12,13 +12,17 @@
|
|||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
org.gradle.jvmargs=-Xmx2048M
|
||||
org.gradle.jvmargs=-Xmx2048M \
|
||||
--add-exports=java.base/sun.nio.ch=ALL-UNNAMED \
|
||||
--add-opens=java.base/java.lang=ALL-UNNAMED \
|
||||
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED \
|
||||
--add-opens=java.base/java.io=ALL-UNNAMED \
|
||||
--add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED
|
||||
android.useAndroidX=true
|
||||
|
||||
minSdkVersion=24
|
||||
minSdkVersion=21
|
||||
targetSdkVersion=28
|
||||
ndkVersion=22.1.7171670
|
||||
compileSdkVersion=30
|
||||
ndkVersion=29.0.14206865
|
||||
compileSdkVersion=36
|
||||
|
||||
markwonVersion=4.6.2
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,5 +1,7 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env sh
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
|
@ -15,81 +15,114 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
|
@ -98,88 +131,118 @@ Please set the JAVA_HOME variable in your environment to match the
|
|||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
|
|
|||
|
|
@ -13,8 +13,10 @@
|
|||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
|
|
@ -25,7 +27,8 @@
|
|||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
|
|
@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
|||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
|
|
@ -56,32 +59,33 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
|
|
|||
|
|
@ -1,2 +1,4 @@
|
|||
jdk:
|
||||
- openjdk17
|
||||
env:
|
||||
JITPACK_NDK_VERSION: "21.1.6352462"
|
||||
JITPACK_NDK_VERSION: "29.0.14206865"
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
|
|
@ -2,6 +2,8 @@ apply plugin: 'com.android.library'
|
|||
apply plugin: 'maven-publish'
|
||||
|
||||
android {
|
||||
namespace "com.termux.emulator"
|
||||
|
||||
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
||||
ndkVersion = System.getenv("JITPACK_NDK_VERSION") ?: project.properties.ndkVersion
|
||||
|
||||
|
|
@ -50,12 +52,13 @@ tasks.withType(Test) {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
implementation "androidx.annotation:annotation:1.9.0"
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
}
|
||||
|
||||
task sourceJar(type: Jar) {
|
||||
from android.sourceSets.main.java.srcDirs
|
||||
classifier "sources"
|
||||
archiveClassifier = "sources"
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
|
|
@ -63,7 +66,7 @@ afterEvaluate {
|
|||
publications {
|
||||
// Creates a Maven publication called "release".
|
||||
release(MavenPublication) {
|
||||
from components.release
|
||||
from components.findByName('release')
|
||||
groupId = 'com.termux'
|
||||
artifactId = 'terminal-emulator'
|
||||
version = '0.118.0'
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
<manifest package="com.termux.terminal">
|
||||
<manifest>
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -23,10 +23,10 @@ final class JNI {
|
|||
* @return the file descriptor resulting from opening /dev/ptmx master device. The sub process will have opened the
|
||||
* slave device counterpart (/dev/pts/$N) and have it as stdint, stdout and stderr.
|
||||
*/
|
||||
public static native int createSubprocess(String cmd, String cwd, String[] args, String[] envVars, int[] processId, int rows, int columns);
|
||||
public static native int createSubprocess(String cmd, String cwd, String[] args, String[] envVars, int[] processId, int rows, int columns, int cellWidth, int cellHeight);
|
||||
|
||||
/** Set the window size for a given pty, which allows connected programs to learn how large their screen is. */
|
||||
public static native void setPtyWindowSize(int fd, int rows, int cols);
|
||||
public static native void setPtyWindowSize(int fd, int rows, int cols, int cellWidth, int cellHeight);
|
||||
|
||||
/**
|
||||
* Causes the calling thread to wait for the process associated with the receiver to finish executing.
|
||||
|
|
|
|||
|
|
@ -227,9 +227,9 @@ public final class KeyHandler {
|
|||
return transformForModifiers("\033[3", keyMode, '~');
|
||||
|
||||
case KEYCODE_PAGE_UP:
|
||||
return "\033[5~";
|
||||
return transformForModifiers("\033[5", keyMode, '~');
|
||||
case KEYCODE_PAGE_DOWN:
|
||||
return "\033[6~";
|
||||
return transformForModifiers("\033[6", keyMode, '~');
|
||||
case KEYCODE_DEL:
|
||||
String prefix = ((keyMode & KEYMOD_ALT) == 0) ? "" : "\033";
|
||||
// Just do what xterm and gnome-terminal does:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
package com.termux.terminal;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
|
||||
public class Logger {
|
||||
|
||||
public static void logError(TerminalSessionClient client, String logTag, String message) {
|
||||
if (client != null)
|
||||
client.logError(logTag, message);
|
||||
else
|
||||
Log.e(logTag, message);
|
||||
}
|
||||
|
||||
public static void logWarn(TerminalSessionClient client, String logTag, String message) {
|
||||
if (client != null)
|
||||
client.logWarn(logTag, message);
|
||||
else
|
||||
Log.w(logTag, message);
|
||||
}
|
||||
|
||||
public static void logInfo(TerminalSessionClient client, String logTag, String message) {
|
||||
if (client != null)
|
||||
client.logInfo(logTag, message);
|
||||
else
|
||||
Log.i(logTag, message);
|
||||
}
|
||||
|
||||
public static void logDebug(TerminalSessionClient client, String logTag, String message) {
|
||||
if (client != null)
|
||||
client.logDebug(logTag, message);
|
||||
else
|
||||
Log.d(logTag, message);
|
||||
}
|
||||
|
||||
public static void logVerbose(TerminalSessionClient client, String logTag, String message) {
|
||||
if (client != null)
|
||||
client.logVerbose(logTag, message);
|
||||
else
|
||||
Log.v(logTag, message);
|
||||
}
|
||||
|
||||
public static void logStackTraceWithMessage(TerminalSessionClient client, String tag, String message, Throwable throwable) {
|
||||
logError(client, tag, getMessageAndStackTraceString(message, throwable));
|
||||
}
|
||||
|
||||
public static String getMessageAndStackTraceString(String message, Throwable throwable) {
|
||||
if (message == null && throwable == null)
|
||||
return null;
|
||||
else if (message != null && throwable != null)
|
||||
return message + ":\n" + getStackTraceString(throwable);
|
||||
else if (throwable == null)
|
||||
return message;
|
||||
else
|
||||
return getStackTraceString(throwable);
|
||||
}
|
||||
|
||||
public static String getStackTraceString(Throwable throwable) {
|
||||
if (throwable == null) return null;
|
||||
|
||||
String stackTraceString = null;
|
||||
|
||||
try {
|
||||
StringWriter errors = new StringWriter();
|
||||
PrintWriter pw = new PrintWriter(errors);
|
||||
throwable.printStackTrace(pw);
|
||||
pw.close();
|
||||
stackTraceString = errors.toString();
|
||||
errors.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return stackTraceString;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -54,7 +54,7 @@ public final class TerminalBuffer {
|
|||
}
|
||||
|
||||
public String getSelectedText(int selX1, int selY1, int selX2, int selY2, boolean joinBackLines) {
|
||||
return getSelectedText(selX1, selY1, selX2, selY2, true, false);
|
||||
return getSelectedText(selX1, selY1, selX2, selY2, joinBackLines, false);
|
||||
}
|
||||
|
||||
public String getSelectedText(int selX1, int selY1, int selX2, int selY2, boolean joinBackLines, boolean joinFullLines) {
|
||||
|
|
@ -93,8 +93,11 @@ public final class TerminalBuffer {
|
|||
if (c != ' ') lastPrintingCharIndex = i;
|
||||
}
|
||||
}
|
||||
if (lastPrintingCharIndex != -1)
|
||||
builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1);
|
||||
|
||||
int len = lastPrintingCharIndex - x1Index + 1;
|
||||
if (lastPrintingCharIndex != -1 && len > 0)
|
||||
builder.append(line, x1Index, len);
|
||||
|
||||
boolean lineFillsWidth = lastPrintingCharIndex == x2Index - 1;
|
||||
if ((!joinBackLines || !rowLineWrap) && (!joinFullLines || !lineFillsWidth)
|
||||
&& row < selY2 && row < mScreenRows - 1) builder.append('\n');
|
||||
|
|
@ -446,8 +449,8 @@ public final class TerminalBuffer {
|
|||
}
|
||||
|
||||
public void setChar(int column, int row, int codePoint, long style) {
|
||||
if (row >= mScreenRows || column >= mColumns)
|
||||
throw new IllegalArgumentException("row=" + row + ", column=" + column + ", mScreenRows=" + mScreenRows + ", mColumns=" + mColumns);
|
||||
if (row < 0 || row >= mScreenRows || column < 0 || column >= mColumns)
|
||||
throw new IllegalArgumentException("TerminalBuffer.setChar(): row=" + row + ", column=" + column + ", mScreenRows=" + mScreenRows + ", mColumns=" + mColumns);
|
||||
row = externalToInternalRow(row);
|
||||
allocateFullLineIfNecessary(row).setChar(column, codePoint, style);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ public final class TerminalColorScheme {
|
|||
|
||||
public void updateWith(Properties props) {
|
||||
reset();
|
||||
boolean cursorPropExists = false;
|
||||
for (Map.Entry<Object, Object> entries : props.entrySet()) {
|
||||
String key = (String) entries.getKey();
|
||||
String value = (String) entries.getValue();
|
||||
|
|
@ -82,6 +83,7 @@ public final class TerminalColorScheme {
|
|||
colorIndex = TextStyle.COLOR_INDEX_BACKGROUND;
|
||||
} else if (key.equals("cursor")) {
|
||||
colorIndex = TextStyle.COLOR_INDEX_CURSOR;
|
||||
cursorPropExists = true;
|
||||
} else if (key.startsWith("color")) {
|
||||
try {
|
||||
colorIndex = Integer.parseInt(key.substring(5));
|
||||
|
|
@ -98,6 +100,27 @@ public final class TerminalColorScheme {
|
|||
|
||||
mDefaultColors[colorIndex] = colorValue;
|
||||
}
|
||||
|
||||
if (!cursorPropExists)
|
||||
setCursorColorForBackground();
|
||||
}
|
||||
|
||||
/**
|
||||
* If the "cursor" color is not set by user, we need to decide on the appropriate color that will
|
||||
* be visible on the current terminal background. White will not be visible on light backgrounds
|
||||
* and black won't be visible on dark backgrounds. So we find the perceived brightness of the
|
||||
* background color and if its below the threshold (too dark), we use white cursor and if its
|
||||
* above (too bright), we use black cursor.
|
||||
*/
|
||||
public void setCursorColorForBackground() {
|
||||
int backgroundColor = mDefaultColors[TextStyle.COLOR_INDEX_BACKGROUND];
|
||||
int brightness = TerminalColors.getPerceivedBrightnessOfColor(backgroundColor);
|
||||
if (brightness > 0) {
|
||||
if (brightness < 130)
|
||||
mDefaultColors[TextStyle.COLOR_INDEX_CURSOR] = 0xffffffff;
|
||||
else
|
||||
mDefaultColors[TextStyle.COLOR_INDEX_CURSOR] = 0xff000000;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.termux.terminal;
|
||||
|
||||
import android.graphics.Color;
|
||||
|
||||
/** Current terminal colors (if different from default). */
|
||||
public final class TerminalColors {
|
||||
|
||||
|
|
@ -73,4 +75,22 @@ public final class TerminalColors {
|
|||
if (c != 0) mCurrentColors[intoIndex] = c;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the perceived brightness of the color based on its RGB components.
|
||||
*
|
||||
* https://www.nbdtech.com/Blog/archive/2008/04/27/Calculating-the-Perceived-Brightness-of-a-Color.aspx
|
||||
* http://alienryderflex.com/hsp.html
|
||||
*
|
||||
* @param color The color code int.
|
||||
* @return Returns value between 0-255.
|
||||
*/
|
||||
public static int getPerceivedBrightnessOfColor(int color) {
|
||||
return (int)
|
||||
Math.floor(Math.sqrt(
|
||||
Math.pow(Color.red(color), 2) * 0.241 +
|
||||
Math.pow(Color.green(color), 2) * 0.691 +
|
||||
Math.pow(Color.blue(color), 2) * 0.068
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,9 +79,17 @@ public final class TerminalEmulator {
|
|||
private static final int ESC_CSI_SINGLE_QUOTE = 18;
|
||||
/** Escape processing: CSI ! */
|
||||
private static final int ESC_CSI_EXCLAMATION = 19;
|
||||
/** Escape processing: "ESC _" or Application Program Command (APC). */
|
||||
private static final int ESC_APC = 20;
|
||||
/** Escape processing: "ESC _" or Application Program Command (APC), followed by Escape. */
|
||||
private static final int ESC_APC_ESCAPE = 21;
|
||||
/** Escape processing: ESC [ <parameter bytes> */
|
||||
private static final int ESC_CSI_UNSUPPORTED_PARAMETER_BYTE = 22;
|
||||
/** Escape processing: ESC [ <parameter bytes> <intermediate bytes> */
|
||||
private static final int ESC_CSI_UNSUPPORTED_INTERMEDIATE_BYTE = 23;
|
||||
|
||||
/** The number of parameter arguments. This name comes from the ANSI standard for terminal escape codes. */
|
||||
private static final int MAX_ESCAPE_PARAMETERS = 16;
|
||||
/** The number of parameter arguments including colon separated sub-parameters. */
|
||||
private static final int MAX_ESCAPE_PARAMETERS = 32;
|
||||
|
||||
/** Needs to be large enough to contain reasonable OSC 52 pastes. */
|
||||
private static final int MAX_OSC_STRING_LENGTH = 8192;
|
||||
|
|
@ -126,13 +134,15 @@ public final class TerminalEmulator {
|
|||
private String mTitle;
|
||||
private final Stack<String> mTitleStack = new Stack<>();
|
||||
|
||||
|
||||
/** The cursor position. Between (0,0) and (mRows-1, mColumns-1). */
|
||||
private int mCursorRow, mCursorCol;
|
||||
|
||||
/** The number of character rows and columns in the terminal screen. */
|
||||
public int mRows, mColumns;
|
||||
|
||||
/** Size of a terminal cell in pixels. */
|
||||
private int mCellWidthPixels, mCellHeightPixels;
|
||||
|
||||
/** The number of terminal transcript rows that can be scrolled back to. */
|
||||
public static final int TERMINAL_TRANSCRIPT_ROWS_MIN = 100;
|
||||
public static final int TERMINAL_TRANSCRIPT_ROWS_MAX = 50000;
|
||||
|
|
@ -172,6 +182,8 @@ public final class TerminalEmulator {
|
|||
private int mArgIndex;
|
||||
/** Holds the arguments of the current escape sequence. */
|
||||
private final int[] mArgs = new int[MAX_ESCAPE_PARAMETERS];
|
||||
/** Holds the bit flags which arguments are sub parameters (after a colon) - bit N is set if <code>mArgs[N]</code> is a sub parameter. */
|
||||
private int mArgsSubParamsBitSet = 0;
|
||||
|
||||
/** Holds OSC and device control arguments, which can be strings. */
|
||||
private final StringBuilder mOSCOrDeviceControlArgs = new StringBuilder();
|
||||
|
|
@ -232,15 +244,17 @@ public final class TerminalEmulator {
|
|||
private boolean mCursorBlinkState;
|
||||
|
||||
/**
|
||||
* Current foreground and background colors. Can either be a color index in [0,259] or a truecolor (24-bit) value.
|
||||
* Current foreground, background and underline colors. Can either be a color index in [0,259] or a truecolor (24-bit) value.
|
||||
* For a 24-bit value the top byte (0xff000000) is set.
|
||||
*
|
||||
* <p>Note that the underline color is currently parsed but not yet used during rendering.
|
||||
*
|
||||
* @see TextStyle
|
||||
*/
|
||||
int mForeColor, mBackColor;
|
||||
int mForeColor, mBackColor, mUnderlineColor;
|
||||
|
||||
/** Current {@link TextStyle} effect. */
|
||||
private int mEffect;
|
||||
int mEffect;
|
||||
|
||||
/**
|
||||
* The number of scrolled lines since last calling {@link #clearScrollCounter()}. Used for moving selection up along
|
||||
|
|
@ -248,6 +262,9 @@ public final class TerminalEmulator {
|
|||
*/
|
||||
private int mScrollCounter = 0;
|
||||
|
||||
/** If automatic scrolling of terminal is disabled */
|
||||
private boolean mAutoScrollDisabled;
|
||||
|
||||
private byte mUtf8ToFollow, mUtf8Index;
|
||||
private final byte[] mUtf8InputBuffer = new byte[4];
|
||||
private int mLastEmittedCodePoint = -1;
|
||||
|
|
@ -308,13 +325,15 @@ public final class TerminalEmulator {
|
|||
}
|
||||
}
|
||||
|
||||
public TerminalEmulator(TerminalOutput session, int columns, int rows, Integer transcriptRows, TerminalSessionClient client) {
|
||||
public TerminalEmulator(TerminalOutput session, int columns, int rows, int cellWidthPixels, int cellHeightPixels, Integer transcriptRows, TerminalSessionClient client) {
|
||||
mSession = session;
|
||||
mScreen = mMainBuffer = new TerminalBuffer(columns, getTerminalTranscriptRows(transcriptRows), rows);
|
||||
mAltBuffer = new TerminalBuffer(columns, rows, rows);
|
||||
mClient = client;
|
||||
mRows = rows;
|
||||
mColumns = columns;
|
||||
mCellWidthPixels = cellWidthPixels;
|
||||
mCellHeightPixels = cellHeightPixels;
|
||||
mTabStop = new boolean[mColumns];
|
||||
reset();
|
||||
}
|
||||
|
|
@ -364,7 +383,10 @@ public final class TerminalEmulator {
|
|||
}
|
||||
}
|
||||
|
||||
public void resize(int columns, int rows) {
|
||||
public void resize(int columns, int rows, int cellWidthPixels, int cellHeightPixels) {
|
||||
this.mCellWidthPixels = cellWidthPixels;
|
||||
this.mCellHeightPixels = cellHeightPixels;
|
||||
|
||||
if (mRows == rows && mColumns == columns) {
|
||||
return;
|
||||
} else if (columns < 2 || rows < 2) {
|
||||
|
|
@ -546,6 +568,15 @@ public final class TerminalEmulator {
|
|||
}
|
||||
|
||||
public void processCodePoint(int b) {
|
||||
// The Application Program-Control (APC) string might be arbitrary non-printable characters, so handle that early.
|
||||
if (mEscapeState == ESC_APC) {
|
||||
doApc(b);
|
||||
return;
|
||||
} else if (mEscapeState == ESC_APC_ESCAPE) {
|
||||
doApcEscape(b);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (b) {
|
||||
case 0: // Null character (NUL, ^@). Do nothing.
|
||||
break;
|
||||
|
|
@ -631,6 +662,10 @@ public final class TerminalEmulator {
|
|||
case ESC_CSI:
|
||||
doCsi(b);
|
||||
break;
|
||||
case ESC_CSI_UNSUPPORTED_PARAMETER_BYTE:
|
||||
case ESC_CSI_UNSUPPORTED_INTERMEDIATE_BYTE:
|
||||
doCsiUnsupportedParameterOrIntermediateByte(b);
|
||||
break;
|
||||
case ESC_CSI_EXCLAMATION:
|
||||
if (b == 'p') { // Soft terminal reset (DECSTR, http://vt100.net/docs/vt510-rm/DECSTR).
|
||||
reset();
|
||||
|
|
@ -796,7 +831,6 @@ public final class TerminalEmulator {
|
|||
int columnsToDelete = Math.min(getArg0(1), columnsAfterCursor);
|
||||
int columnsToMove = columnsAfterCursor - columnsToDelete;
|
||||
mScreen.blockCopy(mCursorCol + columnsToDelete, 0, columnsToMove, mRows, mCursorCol, 0);
|
||||
blockClear(mCursorRow + columnsToMove, 0, columnsToDelete, mRows);
|
||||
} else {
|
||||
unknownSequence(b);
|
||||
}
|
||||
|
|
@ -825,7 +859,7 @@ public final class TerminalEmulator {
|
|||
if (internalBit != -1) {
|
||||
value = isDecsetInternalBitSet(internalBit) ? 1 : 2; // 1=set, 2=reset.
|
||||
} else {
|
||||
mClient.logError(LOG_TAG, "Got DECRQM for unrecognized private DEC mode=" + mode);
|
||||
Logger.logError(mClient, LOG_TAG, "Got DECRQM for unrecognized private DEC mode=" + mode);
|
||||
value = 0; // 0=not recognized, 3=permanently set, 4=permanently reset
|
||||
}
|
||||
}
|
||||
|
|
@ -936,10 +970,17 @@ public final class TerminalEmulator {
|
|||
for (String part : dcs.substring(2).split(";")) {
|
||||
if (part.length() % 2 == 0) {
|
||||
StringBuilder transBuffer = new StringBuilder();
|
||||
char c;
|
||||
for (int i = 0; i < part.length(); i += 2) {
|
||||
char c = (char) Long.decode("0x" + part.charAt(i) + "" + part.charAt(i + 1)).longValue();
|
||||
try {
|
||||
c = (char) Long.decode("0x" + part.charAt(i) + "" + part.charAt(i + 1)).longValue();
|
||||
} catch (NumberFormatException e) {
|
||||
Logger.logStackTraceWithMessage(mClient, LOG_TAG, "Invalid device termcap/terminfo encoded name \"" + part + "\"", e);
|
||||
continue;
|
||||
}
|
||||
transBuffer.append(c);
|
||||
}
|
||||
|
||||
String trans = transBuffer.toString();
|
||||
String responseValue;
|
||||
switch (trans) {
|
||||
|
|
@ -962,7 +1003,7 @@ public final class TerminalEmulator {
|
|||
case "&8": // Undo key - ignore.
|
||||
break;
|
||||
default:
|
||||
mClient.logWarn(LOG_TAG, "Unhandled termcap/terminfo name: '" + trans + "'");
|
||||
Logger.logWarn(mClient, LOG_TAG, "Unhandled termcap/terminfo name: '" + trans + "'");
|
||||
}
|
||||
// Respond with invalid request:
|
||||
mSession.write("\033P0+r" + part + "\033\\");
|
||||
|
|
@ -974,12 +1015,12 @@ public final class TerminalEmulator {
|
|||
mSession.write("\033P1+r" + part + "=" + hexEncoded + "\033\\");
|
||||
}
|
||||
} else {
|
||||
mClient.logError(LOG_TAG, "Invalid device termcap/terminfo name of odd length: " + part);
|
||||
Logger.logError(mClient, LOG_TAG, "Invalid device termcap/terminfo name of odd length: " + part);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (LOG_ESCAPE_SEQUENCES)
|
||||
mClient.logError(LOG_TAG, "Unrecognized device control string: " + dcs);
|
||||
Logger.logError(mClient, LOG_TAG, "Unrecognized device control string: " + dcs);
|
||||
}
|
||||
finishSequence();
|
||||
}
|
||||
|
|
@ -996,12 +1037,67 @@ public final class TerminalEmulator {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When in {@link #ESC_APC} (APC, Application Program Command) sequence.
|
||||
*/
|
||||
private void doApc(int b) {
|
||||
if (b == 27) {
|
||||
continueSequence(ESC_APC_ESCAPE);
|
||||
}
|
||||
// Eat APC sequences silently for now.
|
||||
}
|
||||
|
||||
/**
|
||||
* When in {@link #ESC_APC} (APC, Application Program Command) sequence.
|
||||
*/
|
||||
private void doApcEscape(int b) {
|
||||
if (b == '\\') {
|
||||
// A String Terminator (ST), ending the APC escape sequence.
|
||||
finishSequence();
|
||||
} else {
|
||||
// The Escape character was not the start of a String Terminator (ST),
|
||||
// but instead just data inside of the APC escape sequence.
|
||||
continueSequence(ESC_APC);
|
||||
}
|
||||
}
|
||||
|
||||
private int nextTabStop(int numTabs) {
|
||||
for (int i = mCursorCol + 1; i < mColumns; i++)
|
||||
if (mTabStop[i] && --numTabs == 0) return Math.min(i, mRightMargin);
|
||||
return mRightMargin - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process byte while in the {@link #ESC_CSI_UNSUPPORTED_PARAMETER_BYTE} or
|
||||
* {@link #ESC_CSI_UNSUPPORTED_INTERMEDIATE_BYTE} escape state.
|
||||
*
|
||||
* Parse unsupported parameter, intermediate and final bytes but ignore them.
|
||||
*
|
||||
* > For Control Sequence Introducer, ... the ESC [ is followed by
|
||||
* > - any number (including none) of "parameter bytes" in the range 0x30–0x3F (ASCII 0–9:;<=>?),
|
||||
* > - then by any number of "intermediate bytes" in the range 0x20–0x2F (ASCII space and !"#$%&'()*+,-./),
|
||||
* > - then finally by a single "final byte" in the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~).
|
||||
*
|
||||
* - https://en.wikipedia.org/wiki/ANSI_escape_code#Control_Sequence_Introducer_commands
|
||||
* - https://invisible-island.net/xterm/ecma-48-parameter-format.html#section5.4
|
||||
*/
|
||||
private void doCsiUnsupportedParameterOrIntermediateByte(int b) {
|
||||
if (mEscapeState == ESC_CSI_UNSUPPORTED_PARAMETER_BYTE && b >= 0x30 && b <= 0x3F) {
|
||||
// Supported `0–9:;>?` or unsupported `<=` parameter byte after an
|
||||
// initial unsupported parameter byte in `doCsi()`, or a sequential parameter byte.
|
||||
continueSequence(ESC_CSI_UNSUPPORTED_PARAMETER_BYTE);
|
||||
} else if (b >= 0x20 && b <= 0x2F) {
|
||||
// Optional intermediate byte `!"#$%&'()*+,-./` after parameter or intermediate byte.
|
||||
continueSequence(ESC_CSI_UNSUPPORTED_INTERMEDIATE_BYTE);
|
||||
} else if (b >= 0x40 && b <= 0x7E) {
|
||||
// Final byte `@A–Z[\]^_`a–z{|}~` after parameter or intermediate byte.
|
||||
// Calling `unknownSequence()` would log an error with only a final byte, so ignore it for now.
|
||||
finishSequence();
|
||||
} else {
|
||||
unknownSequence(b);
|
||||
}
|
||||
}
|
||||
|
||||
/** Process byte while in the {@link #ESC_CSI_QUESTIONMARK} escape state. */
|
||||
private void doCsiQuestionMark(int b) {
|
||||
switch (b) {
|
||||
|
|
@ -1069,7 +1165,7 @@ public final class TerminalEmulator {
|
|||
int externalBit = mArgs[i];
|
||||
int internalBit = mapDecSetBitToInternalBit(externalBit);
|
||||
if (internalBit == -1) {
|
||||
mClient.logWarn(LOG_TAG, "Ignoring request to save/recall decset bit=" + externalBit);
|
||||
Logger.logWarn(mClient, LOG_TAG, "Ignoring request to save/recall decset bit=" + externalBit);
|
||||
} else {
|
||||
if (b == 's') {
|
||||
mSavedDecSetFlags |= internalBit;
|
||||
|
|
@ -1259,7 +1355,7 @@ public final class TerminalEmulator {
|
|||
// (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.
|
||||
// (2) enables this feature for keys including the exceptions listed.
|
||||
mClient.logError(LOG_TAG, "(ignored) CSI > MODIFY RESOURCE: " + getArg0(-1) + " to " + getArg1(-1));
|
||||
Logger.logError(mClient, LOG_TAG, "(ignored) CSI > MODIFY RESOURCE: " + getArg0(-1) + " to " + getArg1(-1));
|
||||
break;
|
||||
default:
|
||||
parseArg(b);
|
||||
|
|
@ -1271,6 +1367,7 @@ public final class TerminalEmulator {
|
|||
mEscapeState = ESC;
|
||||
mArgIndex = 0;
|
||||
Arrays.fill(mArgs, -1);
|
||||
mArgsSubParamsBitSet = 0;
|
||||
}
|
||||
|
||||
private void doLinefeed() {
|
||||
|
|
@ -1365,8 +1462,8 @@ public final class TerminalEmulator {
|
|||
// http://www.vt100.net/docs/vt100-ug/chapter3.html: "Move the active position to the same horizontal
|
||||
// position on the preceding line. If the active position is at the top margin, a scroll down is performed".
|
||||
if (mCursorRow <= mTopMargin) {
|
||||
mScreen.blockCopy(0, mTopMargin, mColumns, mBottomMargin - (mTopMargin + 1), 0, mTopMargin + 1);
|
||||
blockClear(0, mTopMargin, mColumns);
|
||||
mScreen.blockCopy(mLeftMargin, mTopMargin, mRightMargin - mLeftMargin, mBottomMargin - (mTopMargin + 1), mLeftMargin, mTopMargin + 1);
|
||||
blockClear(mLeftMargin, mTopMargin, mRightMargin - mLeftMargin);
|
||||
} else {
|
||||
mCursorRow--;
|
||||
}
|
||||
|
|
@ -1391,6 +1488,9 @@ public final class TerminalEmulator {
|
|||
case '>': // DECKPNM
|
||||
setDecsetinternalBit(DECSET_BIT_APPLICATION_KEYPAD, false);
|
||||
break;
|
||||
case '_': // APC - Application Program Command.
|
||||
continueSequence(ESC_APC);
|
||||
break;
|
||||
default:
|
||||
unknownSequence(b);
|
||||
break;
|
||||
|
|
@ -1572,8 +1672,8 @@ public final class TerminalEmulator {
|
|||
final int linesToScrollArg = getArg0(1);
|
||||
final int linesBetweenTopAndBottomMargins = mBottomMargin - mTopMargin;
|
||||
final int linesToScroll = Math.min(linesBetweenTopAndBottomMargins, linesToScrollArg);
|
||||
mScreen.blockCopy(0, mTopMargin, mColumns, linesBetweenTopAndBottomMargins - linesToScroll, 0, mTopMargin + linesToScroll);
|
||||
blockClear(0, mTopMargin, mColumns, linesToScroll);
|
||||
mScreen.blockCopy(mLeftMargin, mTopMargin, mRightMargin - mLeftMargin, linesBetweenTopAndBottomMargins - linesToScroll, mLeftMargin, mTopMargin + linesToScroll);
|
||||
blockClear(mLeftMargin, mTopMargin, mRightMargin - mLeftMargin, linesToScroll);
|
||||
} else {
|
||||
// "${CSI}${func};${startx};${starty};${firstrow};${lastrow}T" - initiate highlight mouse tracking.
|
||||
unimplementedSequence(b);
|
||||
|
|
@ -1595,12 +1695,16 @@ public final class TerminalEmulator {
|
|||
}
|
||||
mCursorCol = newCol;
|
||||
break;
|
||||
case '?': // Esc [ ? -- start of a private mode set
|
||||
case '?': // Esc [ ? -- start of a private parameter byte
|
||||
continueSequence(ESC_CSI_QUESTIONMARK);
|
||||
break;
|
||||
case '>': // "Esc [ >" --
|
||||
case '>': // "Esc [ >" -- start of a private parameter byte
|
||||
continueSequence(ESC_CSI_BIGGERTHAN);
|
||||
break;
|
||||
case '<': // "Esc [ <" -- start of a private parameter byte
|
||||
case '=': // "Esc [ =" -- start of a private parameter byte
|
||||
continueSequence(ESC_CSI_UNSUPPORTED_PARAMETER_BYTE);
|
||||
break;
|
||||
case '`': // Horizontal position absolute (HPA - http://www.vt100.net/docs/vt510-rm/HPA).
|
||||
setCursorColRespectingOriginMode(getArg0(1) - 1);
|
||||
break;
|
||||
|
|
@ -1700,8 +1804,10 @@ public final class TerminalEmulator {
|
|||
mSession.write("\033[3;0;0t");
|
||||
break;
|
||||
case 14: // Report xterm window in pixels. Result is CSI 4 ; height ; width t
|
||||
// We just report characters time 12 here.
|
||||
mSession.write(String.format(Locale.US, "\033[4;%d;%dt", mRows * 12, mColumns * 12));
|
||||
mSession.write(String.format(Locale.US, "\033[4;%d;%dt", mRows * mCellHeightPixels, mColumns * mCellWidthPixels));
|
||||
break;
|
||||
case 16: // Report xterm character cell size in pixels. Result is CSI 6 ; height ; width t
|
||||
mSession.write(String.format(Locale.US, "\033[6;%d;%dt", mCellHeightPixels, mCellWidthPixels));
|
||||
break;
|
||||
case 18: // Report the size of the text area in characters. Result is CSI 8 ; height ; width t
|
||||
mSession.write(String.format(Locale.US, "\033[8;%d;%dt", mRows, mColumns));
|
||||
|
|
@ -1750,7 +1856,12 @@ public final class TerminalEmulator {
|
|||
private void selectGraphicRendition() {
|
||||
if (mArgIndex >= mArgs.length) mArgIndex = mArgs.length - 1;
|
||||
for (int i = 0; i <= mArgIndex; i++) {
|
||||
int code = mArgs[i];
|
||||
// Skip leading sub parameters:
|
||||
if ((mArgsSubParamsBitSet & (1 << i)) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int code = getArg(i, 0, false);
|
||||
if (code < 0) {
|
||||
if (mArgIndex > 0) {
|
||||
continue;
|
||||
|
|
@ -1769,7 +1880,19 @@ public final class TerminalEmulator {
|
|||
} else if (code == 3) {
|
||||
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_ITALIC;
|
||||
} else if (code == 4) {
|
||||
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
|
||||
if (i + 1 <= mArgIndex && ((mArgsSubParamsBitSet & (1 << (i + 1))) != 0)) {
|
||||
// Sub parameter, see https://sw.kovidgoyal.net/kitty/underlines/
|
||||
i++;
|
||||
if (mArgs[i] == 0) {
|
||||
// No underline.
|
||||
mEffect &= ~TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
|
||||
} else {
|
||||
// Different variations of underlines: https://sw.kovidgoyal.net/kitty/underlines/
|
||||
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
|
||||
}
|
||||
} else {
|
||||
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
|
||||
}
|
||||
} else if (code == 5) {
|
||||
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_BLINK;
|
||||
} else if (code == 7) {
|
||||
|
|
@ -1798,40 +1921,43 @@ public final class TerminalEmulator {
|
|||
mEffect &= ~TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH;
|
||||
} else if (code >= 30 && code <= 37) {
|
||||
mForeColor = code - 30;
|
||||
} else if (code == 38 || code == 48) {
|
||||
// Extended set foreground(38)/background (48) color.
|
||||
} else if (code == 38 || code == 48 || code == 58) {
|
||||
// Extended set foreground(38)/background(48)/underline(58) color.
|
||||
// This is followed by either "2;$R;$G;$B" to set a 24-bit color or
|
||||
// "5;$INDEX" to set an indexed color.
|
||||
if (i + 2 > mArgIndex) continue;
|
||||
int firstArg = mArgs[i + 1];
|
||||
if (firstArg == 2) {
|
||||
if (i + 4 > mArgIndex) {
|
||||
mClient.logWarn(LOG_TAG, "Too few CSI" + code + ";2 RGB arguments");
|
||||
Logger.logWarn(mClient, LOG_TAG, "Too few CSI" + code + ";2 RGB arguments");
|
||||
} else {
|
||||
int red = mArgs[i + 2], green = mArgs[i + 3], blue = mArgs[i + 4];
|
||||
int red = getArg(i + 2, 0, false);
|
||||
int green = getArg(i + 3, 0, false);
|
||||
int blue = getArg(i + 4, 0, false);
|
||||
|
||||
if (red < 0 || green < 0 || blue < 0 || red > 255 || green > 255 || blue > 255) {
|
||||
finishSequenceAndLogError("Invalid RGB: " + red + "," + green + "," + blue);
|
||||
} else {
|
||||
int argbColor = 0xff000000 | (red << 16) | (green << 8) | blue;
|
||||
if (code == 38) {
|
||||
mForeColor = argbColor;
|
||||
} else {
|
||||
mBackColor = argbColor;
|
||||
int argbColor = 0xff_00_00_00 | (red << 16) | (green << 8) | blue;
|
||||
switch (code) {
|
||||
case 38: mForeColor = argbColor; break;
|
||||
case 48: mBackColor = argbColor; break;
|
||||
case 58: mUnderlineColor = argbColor; break;
|
||||
}
|
||||
}
|
||||
i += 4; // "2;P_r;P_g;P_r"
|
||||
}
|
||||
} else if (firstArg == 5) {
|
||||
int color = mArgs[i + 2];
|
||||
int color = getArg(i + 2, 0, false);
|
||||
i += 2; // "5;P_s"
|
||||
if (color >= 0 && color < TextStyle.NUM_INDEXED_COLORS) {
|
||||
if (code == 38) {
|
||||
mForeColor = color;
|
||||
} else {
|
||||
mBackColor = color;
|
||||
switch (code) {
|
||||
case 38: mForeColor = color; break;
|
||||
case 48: mBackColor = color; break;
|
||||
case 58: mUnderlineColor = color; break;
|
||||
}
|
||||
} else {
|
||||
if (LOG_ESCAPE_SEQUENCES) mClient.logWarn(LOG_TAG, "Invalid color index: " + color);
|
||||
if (LOG_ESCAPE_SEQUENCES) Logger.logWarn(mClient, LOG_TAG, "Invalid color index: " + color);
|
||||
}
|
||||
} else {
|
||||
finishSequenceAndLogError("Invalid ISO-8613-3 SGR first argument: " + firstArg);
|
||||
|
|
@ -1842,13 +1968,15 @@ public final class TerminalEmulator {
|
|||
mBackColor = code - 40;
|
||||
} else if (code == 49) { // Set default background color.
|
||||
mBackColor = TextStyle.COLOR_INDEX_BACKGROUND;
|
||||
} else if (code == 59) { // Set default underline color.
|
||||
mUnderlineColor = TextStyle.COLOR_INDEX_FOREGROUND;
|
||||
} else if (code >= 90 && code <= 97) { // Bright foreground colors (aixterm codes).
|
||||
mForeColor = code - 90 + 8;
|
||||
} else if (code >= 100 && code <= 107) { // Bright background color (aixterm codes).
|
||||
mBackColor = code - 100 + 8;
|
||||
} else {
|
||||
if (LOG_ESCAPE_SEQUENCES)
|
||||
mClient.logWarn(LOG_TAG, String.format("SGR unknown code %d", code));
|
||||
Logger.logWarn(mClient, LOG_TAG, String.format("SGR unknown code %d", code));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1982,7 +2110,7 @@ public final class TerminalEmulator {
|
|||
String clipboardText = new String(Base64.decode(textParameter.substring(startIndex), 0), StandardCharsets.UTF_8);
|
||||
mSession.onCopyTextToClipboard(clipboardText);
|
||||
} catch (Exception e) {
|
||||
mClient.logError(LOG_TAG, "OSC Manipulate selection, invalid string '" + textParameter + "");
|
||||
Logger.logError(mClient, LOG_TAG, "OSC Manipulate selection, invalid string '" + textParameter + "");
|
||||
}
|
||||
break;
|
||||
case 104:
|
||||
|
|
@ -2077,17 +2205,36 @@ public final class TerminalEmulator {
|
|||
|
||||
private void scrollDownOneLine() {
|
||||
mScrollCounter++;
|
||||
long currentStyle = getStyle();
|
||||
if (mLeftMargin != 0 || mRightMargin != mColumns) {
|
||||
// Horizontal margin: Do not put anything into scroll history, just non-margin part of screen up.
|
||||
mScreen.blockCopy(mLeftMargin, mTopMargin + 1, mRightMargin - mLeftMargin, mBottomMargin - mTopMargin - 1, mLeftMargin, mTopMargin);
|
||||
// .. and blank bottom row between margins:
|
||||
mScreen.blockSet(mLeftMargin, mBottomMargin - 1, mRightMargin - mLeftMargin, 1, ' ', mEffect);
|
||||
mScreen.blockSet(mLeftMargin, mBottomMargin - 1, mRightMargin - mLeftMargin, 1, ' ', currentStyle);
|
||||
} else {
|
||||
mScreen.scrollDownOneLine(mTopMargin, mBottomMargin, getStyle());
|
||||
mScreen.scrollDownOneLine(mTopMargin, mBottomMargin, currentStyle);
|
||||
}
|
||||
}
|
||||
|
||||
/** Process the next ASCII character of a parameter. */
|
||||
/**
|
||||
* Process the next ASCII character of a parameter.
|
||||
*
|
||||
* <p>You must use the ; character to separate parameters and : to separate sub-parameters.
|
||||
*
|
||||
* <p>Parameter characters modify the action or interpretation of the sequence. Originally
|
||||
* you can use up to 16 parameters per sequence, but following at least xterm and alacritty
|
||||
* we use a common space for parameters and sub-parameters, allowing 32 in total.
|
||||
*
|
||||
* <p>All parameters are unsigned, positive decimal integers, with the most significant
|
||||
* digit sent first. Any parameter greater than 9999 (decimal) is set to 9999
|
||||
* (decimal). If you do not specify a value, a 0 value is assumed. A 0 value
|
||||
* or omitted parameter indicates a default value for the sequence. For most
|
||||
* sequences, the default value is 1.
|
||||
*
|
||||
* <p>References:
|
||||
* <a href="https://vt100.net/docs/vt510-rm/chapter4.html#S4.3.3">VT510 Video Terminal Programmer Information: Control Sequences</a>
|
||||
* <a href="https://github.com/alacritty/vte/issues/22">alacritty/vte: Implement colon separated CSI parameters</a>
|
||||
* */
|
||||
private void parseArg(int b) {
|
||||
if (b >= '0' && b <= '9') {
|
||||
if (mArgIndex < mArgs.length) {
|
||||
|
|
@ -2099,12 +2246,19 @@ public final class TerminalEmulator {
|
|||
} else {
|
||||
value = thisDigit;
|
||||
}
|
||||
if (value > 9999)
|
||||
value = 9999;
|
||||
mArgs[mArgIndex] = value;
|
||||
}
|
||||
continueSequence(mEscapeState);
|
||||
} else if (b == ';') {
|
||||
if (mArgIndex < mArgs.length) {
|
||||
} else if (b == ';' || b == ':') {
|
||||
if (mArgIndex + 1 < mArgs.length) {
|
||||
mArgIndex++;
|
||||
if (b == ':') {
|
||||
mArgsSubParamsBitSet |= 1 << mArgIndex;
|
||||
}
|
||||
} else {
|
||||
logError("Too many parameters when in state: " + mEscapeState);
|
||||
}
|
||||
continueSequence(mEscapeState);
|
||||
} else {
|
||||
|
|
@ -2178,7 +2332,7 @@ public final class TerminalEmulator {
|
|||
}
|
||||
|
||||
private void finishSequenceAndLogError(String error) {
|
||||
if (LOG_ESCAPE_SEQUENCES) mClient.logWarn(LOG_TAG, error);
|
||||
if (LOG_ESCAPE_SEQUENCES) Logger.logWarn(mClient, LOG_TAG, error);
|
||||
finishSequence();
|
||||
}
|
||||
|
||||
|
|
@ -2326,7 +2480,14 @@ public final class TerminalEmulator {
|
|||
}
|
||||
|
||||
int offsetDueToCombiningChar = ((displayWidth <= 0 && mCursorCol > 0 && !mAboutToAutoWrap) ? 1 : 0);
|
||||
mScreen.setChar(mCursorCol - offsetDueToCombiningChar, mCursorRow, codePoint, getStyle());
|
||||
int column = mCursorCol - offsetDueToCombiningChar;
|
||||
|
||||
// Fix TerminalRow.setChar() ArrayIndexOutOfBoundsException index=-1 exception reported
|
||||
// The offsetDueToCombiningChar would never be 1 if mCursorCol was 0 to get column/index=-1,
|
||||
// so was mCursorCol changed after the offsetDueToCombiningChar conditional by another thread?
|
||||
// TODO: Check if there are thread synchronization issues with mCursorCol and mCursorRow, possibly causing others bugs too.
|
||||
if (column < 0) column = 0;
|
||||
mScreen.setChar(column, mCursorRow, codePoint, getStyle());
|
||||
|
||||
if (autoWrap && displayWidth > 0)
|
||||
mAboutToAutoWrap = (mCursorCol == mRightMargin - displayWidth);
|
||||
|
|
@ -2364,6 +2525,15 @@ public final class TerminalEmulator {
|
|||
mScrollCounter = 0;
|
||||
}
|
||||
|
||||
public boolean isAutoScrollDisabled() {
|
||||
return mAutoScrollDisabled;
|
||||
}
|
||||
|
||||
public void toggleAutoScrollDisabled() {
|
||||
mAutoScrollDisabled = !mAutoScrollDisabled;
|
||||
}
|
||||
|
||||
|
||||
/** Reset terminal state so user can interact with it regardless of present state. */
|
||||
public void reset() {
|
||||
setCursorStyle();
|
||||
|
|
|
|||
|
|
@ -11,11 +11,37 @@ public final class TerminalRow {
|
|||
|
||||
private static final float SPARE_CAPACITY_FACTOR = 1.5f;
|
||||
|
||||
/**
|
||||
* Max combining characters that can exist in a column, that are separate from the base character
|
||||
* itself. Any additional combining characters will be ignored and not added to the column.
|
||||
*
|
||||
* There does not seem to be limit in unicode standard for max number of combination characters
|
||||
* that can be combined but such characters are primarily under 10.
|
||||
*
|
||||
* "Section 3.6 Combination" of unicode standard contains combining characters info.
|
||||
* - https://www.unicode.org/versions/Unicode15.0.0/ch03.pdf
|
||||
* - https://en.wikipedia.org/wiki/Combining_character#Unicode_ranges
|
||||
* - https://stackoverflow.com/questions/71237212/what-is-the-maximum-number-of-unicode-combined-characters-that-may-be-needed-to
|
||||
*
|
||||
* UAX15-D3 Stream-Safe Text Format limits to max 30 combining characters.
|
||||
* > The value of 30 is chosen to be significantly beyond what is required for any linguistic or technical usage.
|
||||
* > While it would have been feasible to chose a smaller number, this value provides a very wide margin,
|
||||
* > yet is well within the buffer size limits of practical implementations.
|
||||
* - https://unicode.org/reports/tr15/#Stream_Safe_Text_Format
|
||||
* - https://stackoverflow.com/a/11983435/14686958
|
||||
*
|
||||
* We choose the value 15 because it should be enough for terminal based applications and keep
|
||||
* the memory usage low for a terminal row, won't affect performance or cause terminal to
|
||||
* lag or hang, and will keep malicious applications from causing harm. The value can be
|
||||
* increased if ever needed for legitimate applications.
|
||||
*/
|
||||
private static final int MAX_COMBINING_CHARACTERS_PER_COLUMN = 15;
|
||||
|
||||
/** The number of columns in this terminal row. */
|
||||
private final int mColumns;
|
||||
/** The text filling this terminal row. */
|
||||
public char[] mText;
|
||||
/** The number of java char:s used in {@link #mText}. */
|
||||
/** The number of java chars used in {@link #mText}. */
|
||||
private short mSpaceUsed;
|
||||
/** If this row has been line wrapped due to text output at the end of line. */
|
||||
boolean mLineWrap;
|
||||
|
|
@ -124,6 +150,9 @@ public final class TerminalRow {
|
|||
|
||||
// https://github.com/steven676/Android-Terminal-Emulator/commit/9a47042620bec87617f0b4f5d50568535668fe26
|
||||
public void setChar(int columnToSet, int codePoint, long style) {
|
||||
if (columnToSet < 0 || columnToSet >= mStyle.length)
|
||||
throw new IllegalArgumentException("TerminalRow.setChar(): columnToSet=" + columnToSet + ", codePoint=" + codePoint + ", style=" + style);
|
||||
|
||||
mStyle[columnToSet] = style;
|
||||
|
||||
final int newCodePointDisplayWidth = WcWidth.width(codePoint);
|
||||
|
|
@ -160,18 +189,25 @@ public final class TerminalRow {
|
|||
// Get the number of elements in the mText array this column uses now
|
||||
int oldCharactersUsedForColumn;
|
||||
if (columnToSet + oldCodePointDisplayWidth < mColumns) {
|
||||
oldCharactersUsedForColumn = findStartOfColumn(columnToSet + oldCodePointDisplayWidth) - oldStartOfColumnIndex;
|
||||
int oldEndOfColumnIndex = findStartOfColumn(columnToSet + oldCodePointDisplayWidth);
|
||||
oldCharactersUsedForColumn = oldEndOfColumnIndex - oldStartOfColumnIndex;
|
||||
} else {
|
||||
// Last character.
|
||||
oldCharactersUsedForColumn = mSpaceUsed - oldStartOfColumnIndex;
|
||||
}
|
||||
|
||||
// If MAX_COMBINING_CHARACTERS_PER_COLUMN already exist in column, then ignore adding additional combining characters.
|
||||
if (newIsCombining) {
|
||||
int combiningCharsCount = WcWidth.zeroWidthCharsCount(mText, oldStartOfColumnIndex, oldStartOfColumnIndex + oldCharactersUsedForColumn);
|
||||
if (combiningCharsCount >= MAX_COMBINING_CHARACTERS_PER_COLUMN)
|
||||
return;
|
||||
}
|
||||
|
||||
// Find how many chars this column will need
|
||||
int newCharactersUsedForColumn = Character.charCount(codePoint);
|
||||
if (newIsCombining) {
|
||||
// Combining characters are added to the contents of the column instead of overwriting them, so that they
|
||||
// modify the existing contents.
|
||||
// FIXME: Put a limit of combining characters.
|
||||
// FIXME: Unassigned characters also get width=0.
|
||||
newCharactersUsedForColumn += oldCharactersUsedForColumn;
|
||||
}
|
||||
|
|
@ -186,7 +222,7 @@ public final class TerminalRow {
|
|||
if (mSpaceUsed + javaCharDifference > text.length) {
|
||||
// We need to grow the array
|
||||
char[] newText = new char[text.length + mColumns];
|
||||
System.arraycopy(text, 0, newText, 0, oldStartOfColumnIndex + oldCharactersUsedForColumn);
|
||||
System.arraycopy(text, 0, newText, 0, oldNextColumnIndex);
|
||||
System.arraycopy(text, oldNextColumnIndex, newText, newNextColumnIndex, oldCharactersAfterColumn);
|
||||
mText = text = newText;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import java.util.UUID;
|
|||
* A terminal session, consisting of a process coupled to a terminal interface.
|
||||
* <p>
|
||||
* The subprocess will be executed by the constructor, and when the size is made known by a call to
|
||||
* {@link #updateSize(int, int)} terminal emulation will begin and threads will be spawned to handle the subprocess I/O.
|
||||
* {@link #updateSize(int, int, int, int)} terminal emulation will begin and threads will be spawned to handle the subprocess I/O.
|
||||
* All terminal emulation and callback methods will be performed on the main thread.
|
||||
* <p>
|
||||
* The child process may be exited forcefully by using the {@link #finishIfRunning()} method.
|
||||
|
|
@ -61,7 +61,7 @@ public final class TerminalSession extends TerminalOutput {
|
|||
|
||||
/**
|
||||
* The file descriptor referencing the master half of a pseudo-terminal pair, resulting from calling
|
||||
* {@link JNI#createSubprocess(String, String, String[], String[], int[], int, int)}.
|
||||
* {@link JNI#createSubprocess(String, String, String[], String[], int[], int, int, int, int)}.
|
||||
*/
|
||||
private int mTerminalFileDescriptor;
|
||||
|
||||
|
|
@ -100,12 +100,12 @@ public final class TerminalSession extends TerminalOutput {
|
|||
}
|
||||
|
||||
/** Inform the attached pty of the new size and reflow or initialize the emulator. */
|
||||
public void updateSize(int columns, int rows) {
|
||||
public void updateSize(int columns, int rows, int cellWidthPixels, int cellHeightPixels) {
|
||||
if (mEmulator == null) {
|
||||
initializeEmulator(columns, rows);
|
||||
initializeEmulator(columns, rows, cellWidthPixels, cellHeightPixels);
|
||||
} else {
|
||||
JNI.setPtyWindowSize(mTerminalFileDescriptor, rows, columns);
|
||||
mEmulator.resize(columns, rows);
|
||||
JNI.setPtyWindowSize(mTerminalFileDescriptor, rows, columns, cellWidthPixels, cellHeightPixels);
|
||||
mEmulator.resize(columns, rows, cellWidthPixels, cellHeightPixels);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -120,12 +120,13 @@ public final class TerminalSession extends TerminalOutput {
|
|||
* @param columns The number of columns in the terminal window.
|
||||
* @param rows The number of rows in the terminal window.
|
||||
*/
|
||||
public void initializeEmulator(int columns, int rows) {
|
||||
mEmulator = new TerminalEmulator(this, columns, rows, mTranscriptRows, mClient);
|
||||
public void initializeEmulator(int columns, int rows, int cellWidthPixels, int cellHeightPixels) {
|
||||
mEmulator = new TerminalEmulator(this, columns, rows, cellWidthPixels, cellHeightPixels, mTranscriptRows, mClient);
|
||||
|
||||
int[] processId = new int[1];
|
||||
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);
|
||||
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns, cellWidthPixels, cellHeightPixels);
|
||||
mShellPid = processId[0];
|
||||
mClient.setTerminalShellPid(this, mShellPid);
|
||||
|
||||
final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor, mClient);
|
||||
|
||||
|
|
@ -236,7 +237,7 @@ public final class TerminalSession extends TerminalOutput {
|
|||
try {
|
||||
Os.kill(mShellPid, OsConstants.SIGKILL);
|
||||
} catch (ErrnoException e) {
|
||||
mClient.logWarn(LOG_TAG, "Failed sending SIGKILL: " + e.getMessage());
|
||||
Logger.logWarn(mClient, LOG_TAG, "Failed sending SIGKILL: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -308,7 +309,7 @@ public final class TerminalSession extends TerminalOutput {
|
|||
return outputPath;
|
||||
}
|
||||
} catch (IOException | SecurityException e) {
|
||||
mClient.logStackTraceWithMessage(LOG_TAG, "Error getting current directory", e);
|
||||
Logger.logStackTraceWithMessage(mClient, LOG_TAG, "Error getting current directory", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -326,7 +327,7 @@ public final class TerminalSession extends TerminalOutput {
|
|||
descriptorField.setAccessible(true);
|
||||
descriptorField.set(result, fileDescriptor);
|
||||
} catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) {
|
||||
client.logStackTraceWithMessage(LOG_TAG, "Error accessing FileDescriptor#descriptor private field", e);
|
||||
Logger.logStackTraceWithMessage(client, LOG_TAG, "Error accessing FileDescriptor#descriptor private field", e);
|
||||
System.exit(1);
|
||||
}
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
package com.termux.terminal;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
|
@ -7,22 +10,24 @@ package com.termux.terminal;
|
|||
*/
|
||||
public interface TerminalSessionClient {
|
||||
|
||||
void onTextChanged(TerminalSession changedSession);
|
||||
void onTextChanged(@NonNull TerminalSession changedSession);
|
||||
|
||||
void onTitleChanged(TerminalSession changedSession);
|
||||
void onTitleChanged(@NonNull TerminalSession changedSession);
|
||||
|
||||
void onSessionFinished(TerminalSession finishedSession);
|
||||
void onSessionFinished(@NonNull TerminalSession finishedSession);
|
||||
|
||||
void onCopyTextToClipboard(TerminalSession session, String text);
|
||||
void onCopyTextToClipboard(@NonNull TerminalSession session, String text);
|
||||
|
||||
void onPasteTextFromClipboard(TerminalSession session);
|
||||
void onPasteTextFromClipboard(@Nullable TerminalSession session);
|
||||
|
||||
void onBell(TerminalSession session);
|
||||
void onBell(@NonNull TerminalSession session);
|
||||
|
||||
void onColorsChanged(TerminalSession session);
|
||||
void onColorsChanged(@NonNull TerminalSession session);
|
||||
|
||||
void onTerminalCursorStateChange(boolean state);
|
||||
|
||||
void setTerminalShellPid(@NonNull TerminalSession session, int pid);
|
||||
|
||||
|
||||
|
||||
Integer getTerminalCursorStyle();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.termux.terminal;
|
||||
|
||||
/**
|
||||
* Implementation of wcwidth(3) for Unicode 9.
|
||||
* Implementation of wcwidth(3) for Unicode 15.
|
||||
*
|
||||
* Implementation from https://github.com/jquast/wcwidth but we return 0 for unprintable characters.
|
||||
*
|
||||
|
|
@ -9,12 +9,13 @@ package com.termux.terminal;
|
|||
* Must be kept in sync with the following:
|
||||
* https://github.com/termux/wcwidth
|
||||
* https://github.com/termux/libandroid-support
|
||||
* https://github.com/termux/termux-packages/tree/master/libandroid-support
|
||||
* https://github.com/termux/termux-packages/tree/master/packages/libandroid-support
|
||||
*/
|
||||
public final class WcWidth {
|
||||
|
||||
// From https://github.com/jquast/wcwidth/blob/master/wcwidth/table_zero.py
|
||||
// at commit b29897e5a1b403a0e36f7fc991614981cbc42475 (2020-07-14):
|
||||
// from https://github.com/jquast/wcwidth/pull/64
|
||||
// at commit 1b9b6585b0080ea5cb88dc9815796505724793fe (2022-12-16):
|
||||
private static final int[][] ZERO_WIDTH = {
|
||||
{0x00300, 0x0036f}, // Combining Grave Accent ..Combining Latin Small Le
|
||||
{0x00483, 0x00489}, // Combining Cyrillic Titlo..Combining Cyrillic Milli
|
||||
|
|
@ -40,7 +41,8 @@ public final class WcWidth {
|
|||
{0x00825, 0x00827}, // Samaritan Vowel Sign Sho..Samaritan Vowel Sign U
|
||||
{0x00829, 0x0082d}, // Samaritan Vowel Sign Lon..Samaritan Mark Nequdaa
|
||||
{0x00859, 0x0085b}, // Mandaic Affrication Mark..Mandaic Gemination Mark
|
||||
{0x008d3, 0x008e1}, // Arabic Small Low Waw ..Arabic Small High Sign S
|
||||
{0x00898, 0x0089f}, // Arabic Small High Word A..Arabic Half Madda Over M
|
||||
{0x008ca, 0x008e1}, // Arabic Small High Farsi ..Arabic Small High Sign S
|
||||
{0x008e3, 0x00902}, // Arabic Turned Damma Belo..Devanagari Sign Anusvara
|
||||
{0x0093a, 0x0093a}, // Devanagari Vowel Sign Oe..Devanagari Vowel Sign Oe
|
||||
{0x0093c, 0x0093c}, // Devanagari Sign Nukta ..Devanagari Sign Nukta
|
||||
|
|
@ -74,13 +76,14 @@ public final class WcWidth {
|
|||
{0x00b3f, 0x00b3f}, // Oriya Vowel Sign I ..Oriya Vowel Sign I
|
||||
{0x00b41, 0x00b44}, // Oriya Vowel Sign U ..Oriya Vowel Sign Vocalic
|
||||
{0x00b4d, 0x00b4d}, // Oriya Sign Virama ..Oriya Sign Virama
|
||||
{0x00b55, 0x00b56}, // (nil) ..Oriya Ai Length Mark
|
||||
{0x00b55, 0x00b56}, // Oriya Sign Overline ..Oriya Ai Length Mark
|
||||
{0x00b62, 0x00b63}, // Oriya Vowel Sign Vocalic..Oriya Vowel Sign Vocalic
|
||||
{0x00b82, 0x00b82}, // Tamil Sign Anusvara ..Tamil Sign Anusvara
|
||||
{0x00bc0, 0x00bc0}, // Tamil Vowel Sign Ii ..Tamil Vowel Sign Ii
|
||||
{0x00bcd, 0x00bcd}, // Tamil Sign Virama ..Tamil Sign Virama
|
||||
{0x00c00, 0x00c00}, // Telugu Sign Combining Ca..Telugu Sign Combining Ca
|
||||
{0x00c04, 0x00c04}, // Telugu Sign Combining An..Telugu Sign Combining An
|
||||
{0x00c3c, 0x00c3c}, // Telugu Sign Nukta ..Telugu Sign Nukta
|
||||
{0x00c3e, 0x00c40}, // Telugu Vowel Sign Aa ..Telugu Vowel Sign Ii
|
||||
{0x00c46, 0x00c48}, // Telugu Vowel Sign E ..Telugu Vowel Sign Ai
|
||||
{0x00c4a, 0x00c4d}, // Telugu Vowel Sign O ..Telugu Sign Virama
|
||||
|
|
@ -97,7 +100,7 @@ public final class WcWidth {
|
|||
{0x00d41, 0x00d44}, // Malayalam Vowel Sign U ..Malayalam Vowel Sign Voc
|
||||
{0x00d4d, 0x00d4d}, // Malayalam Sign Virama ..Malayalam Sign Virama
|
||||
{0x00d62, 0x00d63}, // Malayalam Vowel Sign Voc..Malayalam Vowel Sign Voc
|
||||
{0x00d81, 0x00d81}, // (nil) ..(nil)
|
||||
{0x00d81, 0x00d81}, // Sinhala Sign Candrabindu..Sinhala Sign Candrabindu
|
||||
{0x00dca, 0x00dca}, // Sinhala Sign Al-lakuna ..Sinhala Sign Al-lakuna
|
||||
{0x00dd2, 0x00dd4}, // Sinhala Vowel Sign Ketti..Sinhala Vowel Sign Ketti
|
||||
{0x00dd6, 0x00dd6}, // Sinhala Vowel Sign Diga ..Sinhala Vowel Sign Diga
|
||||
|
|
@ -106,7 +109,7 @@ public final class WcWidth {
|
|||
{0x00e47, 0x00e4e}, // Thai Character Maitaikhu..Thai Character Yamakkan
|
||||
{0x00eb1, 0x00eb1}, // Lao Vowel Sign Mai Kan ..Lao Vowel Sign Mai Kan
|
||||
{0x00eb4, 0x00ebc}, // Lao Vowel Sign I ..Lao Semivowel Sign Lo
|
||||
{0x00ec8, 0x00ecd}, // Lao Tone Mai Ek ..Lao Niggahita
|
||||
{0x00ec8, 0x00ece}, // Lao Tone Mai Ek ..(nil)
|
||||
{0x00f18, 0x00f19}, // Tibetan Astrological Sig..Tibetan Astrological Sig
|
||||
{0x00f35, 0x00f35}, // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung
|
||||
{0x00f37, 0x00f37}, // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung
|
||||
|
|
@ -130,7 +133,7 @@ public final class WcWidth {
|
|||
{0x0109d, 0x0109d}, // Myanmar Vowel Sign Aiton..Myanmar Vowel Sign Aiton
|
||||
{0x0135d, 0x0135f}, // Ethiopic Combining Gemin..Ethiopic Combining Gemin
|
||||
{0x01712, 0x01714}, // Tagalog Vowel Sign I ..Tagalog Sign Virama
|
||||
{0x01732, 0x01734}, // Hanunoo Vowel Sign I ..Hanunoo Sign Pamudpod
|
||||
{0x01732, 0x01733}, // Hanunoo Vowel Sign I ..Hanunoo Vowel Sign U
|
||||
{0x01752, 0x01753}, // Buhid Vowel Sign I ..Buhid Vowel Sign U
|
||||
{0x01772, 0x01773}, // Tagbanwa Vowel Sign I ..Tagbanwa Vowel Sign U
|
||||
{0x017b4, 0x017b5}, // Khmer Vowel Inherent Aq ..Khmer Vowel Inherent Aa
|
||||
|
|
@ -139,6 +142,7 @@ public final class WcWidth {
|
|||
{0x017c9, 0x017d3}, // Khmer Sign Muusikatoan ..Khmer Sign Bathamasat
|
||||
{0x017dd, 0x017dd}, // Khmer Sign Atthacan ..Khmer Sign Atthacan
|
||||
{0x0180b, 0x0180d}, // Mongolian Free Variation..Mongolian Free Variation
|
||||
{0x0180f, 0x0180f}, // Mongolian Free Variation..Mongolian Free Variation
|
||||
{0x01885, 0x01886}, // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal
|
||||
{0x018a9, 0x018a9}, // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal
|
||||
{0x01920, 0x01922}, // Limbu Vowel Sign A ..Limbu Vowel Sign U
|
||||
|
|
@ -154,7 +158,7 @@ public final class WcWidth {
|
|||
{0x01a65, 0x01a6c}, // Tai Tham Vowel Sign I ..Tai Tham Vowel Sign Oa B
|
||||
{0x01a73, 0x01a7c}, // Tai Tham Vowel Sign Oa A..Tai Tham Sign Khuen-lue
|
||||
{0x01a7f, 0x01a7f}, // Tai Tham Combining Crypt..Tai Tham Combining Crypt
|
||||
{0x01ab0, 0x01ac0}, // Combining Doubled Circum..(nil)
|
||||
{0x01ab0, 0x01ace}, // Combining Doubled Circum..Combining Latin Small Le
|
||||
{0x01b00, 0x01b03}, // Balinese Sign Ulu Ricem ..Balinese Sign Surang
|
||||
{0x01b34, 0x01b34}, // Balinese Sign Rerekan ..Balinese Sign Rerekan
|
||||
{0x01b36, 0x01b3a}, // Balinese Vowel Sign Ulu ..Balinese Vowel Sign Ra R
|
||||
|
|
@ -177,8 +181,7 @@ public final class WcWidth {
|
|||
{0x01ced, 0x01ced}, // Vedic Sign Tiryak ..Vedic Sign Tiryak
|
||||
{0x01cf4, 0x01cf4}, // Vedic Tone Candra Above ..Vedic Tone Candra Above
|
||||
{0x01cf8, 0x01cf9}, // Vedic Tone Ring Above ..Vedic Tone Double Ring A
|
||||
{0x01dc0, 0x01df9}, // Combining Dotted Grave A..Combining Wide Inverted
|
||||
{0x01dfb, 0x01dff}, // Combining Deletion Mark ..Combining Right Arrowhea
|
||||
{0x01dc0, 0x01dff}, // Combining Dotted Grave A..Combining Right Arrowhea
|
||||
{0x020d0, 0x020f0}, // Combining Left Harpoon A..Combining Asterisk Above
|
||||
{0x02cef, 0x02cf1}, // Coptic Combining Ni Abov..Coptic Combining Spiritu
|
||||
{0x02d7f, 0x02d7f}, // Tifinagh Consonant Joine..Tifinagh Consonant Joine
|
||||
|
|
@ -193,7 +196,7 @@ public final class WcWidth {
|
|||
{0x0a806, 0x0a806}, // Syloti Nagri Sign Hasant..Syloti Nagri Sign Hasant
|
||||
{0x0a80b, 0x0a80b}, // Syloti Nagri Sign Anusva..Syloti Nagri Sign Anusva
|
||||
{0x0a825, 0x0a826}, // Syloti Nagri Vowel Sign ..Syloti Nagri Vowel Sign
|
||||
{0x0a82c, 0x0a82c}, // (nil) ..(nil)
|
||||
{0x0a82c, 0x0a82c}, // Syloti Nagri Sign Altern..Syloti Nagri Sign Altern
|
||||
{0x0a8c4, 0x0a8c5}, // Saurashtra Sign Virama ..Saurashtra Sign Candrabi
|
||||
{0x0a8e0, 0x0a8f1}, // Combining Devanagari Dig..Combining Devanagari Sig
|
||||
{0x0a8ff, 0x0a8ff}, // Devanagari Vowel Sign Ay..Devanagari Vowel Sign Ay
|
||||
|
|
@ -233,13 +236,18 @@ public final class WcWidth {
|
|||
{0x10a3f, 0x10a3f}, // Kharoshthi Virama ..Kharoshthi Virama
|
||||
{0x10ae5, 0x10ae6}, // Manichaean Abbreviation ..Manichaean Abbreviation
|
||||
{0x10d24, 0x10d27}, // Hanifi Rohingya Sign Har..Hanifi Rohingya Sign Tas
|
||||
{0x10eab, 0x10eac}, // (nil) ..(nil)
|
||||
{0x10eab, 0x10eac}, // Yezidi Combining Hamza M..Yezidi Combining Madda M
|
||||
{0x10efd, 0x10eff}, // (nil) ..(nil)
|
||||
{0x10f46, 0x10f50}, // Sogdian Combining Dot Be..Sogdian Combining Stroke
|
||||
{0x10f82, 0x10f85}, // Old Uyghur Combining Dot..Old Uyghur Combining Two
|
||||
{0x11001, 0x11001}, // Brahmi Sign Anusvara ..Brahmi Sign Anusvara
|
||||
{0x11038, 0x11046}, // Brahmi Vowel Sign Aa ..Brahmi Virama
|
||||
{0x11070, 0x11070}, // Brahmi Sign Old Tamil Vi..Brahmi Sign Old Tamil Vi
|
||||
{0x11073, 0x11074}, // Brahmi Vowel Sign Old Ta..Brahmi Vowel Sign Old Ta
|
||||
{0x1107f, 0x11081}, // Brahmi Number Joiner ..Kaithi Sign Anusvara
|
||||
{0x110b3, 0x110b6}, // Kaithi Vowel Sign U ..Kaithi Vowel Sign Ai
|
||||
{0x110b9, 0x110ba}, // Kaithi Sign Virama ..Kaithi Sign Nukta
|
||||
{0x110c2, 0x110c2}, // Kaithi Vowel Sign Vocali..Kaithi Vowel Sign Vocali
|
||||
{0x11100, 0x11102}, // Chakma Sign Candrabindu ..Chakma Sign Visarga
|
||||
{0x11127, 0x1112b}, // Chakma Vowel Sign A ..Chakma Vowel Sign Uu
|
||||
{0x1112d, 0x11134}, // Chakma Vowel Sign Ai ..Chakma Maayyaa
|
||||
|
|
@ -247,11 +255,12 @@ public final class WcWidth {
|
|||
{0x11180, 0x11181}, // Sharada Sign Candrabindu..Sharada Sign Anusvara
|
||||
{0x111b6, 0x111be}, // Sharada Vowel Sign U ..Sharada Vowel Sign O
|
||||
{0x111c9, 0x111cc}, // Sharada Sandhi Mark ..Sharada Extra Short Vowe
|
||||
{0x111cf, 0x111cf}, // (nil) ..(nil)
|
||||
{0x111cf, 0x111cf}, // Sharada Sign Inverted Ca..Sharada Sign Inverted Ca
|
||||
{0x1122f, 0x11231}, // Khojki Vowel Sign U ..Khojki Vowel Sign Ai
|
||||
{0x11234, 0x11234}, // Khojki Sign Anusvara ..Khojki Sign Anusvara
|
||||
{0x11236, 0x11237}, // Khojki Sign Nukta ..Khojki Sign Shadda
|
||||
{0x1123e, 0x1123e}, // Khojki Sign Sukun ..Khojki Sign Sukun
|
||||
{0x11241, 0x11241}, // (nil) ..(nil)
|
||||
{0x112df, 0x112df}, // Khudawadi Sign Anusvara ..Khudawadi Sign Anusvara
|
||||
{0x112e3, 0x112ea}, // Khudawadi Vowel Sign U ..Khudawadi Sign Virama
|
||||
{0x11300, 0x11301}, // Grantha Sign Combining A..Grantha Sign Candrabindu
|
||||
|
|
@ -283,9 +292,9 @@ public final class WcWidth {
|
|||
{0x11727, 0x1172b}, // Ahom Vowel Sign Aw ..Ahom Sign Killer
|
||||
{0x1182f, 0x11837}, // Dogra Vowel Sign U ..Dogra Sign Anusvara
|
||||
{0x11839, 0x1183a}, // Dogra Sign Virama ..Dogra Sign Nukta
|
||||
{0x1193b, 0x1193c}, // (nil) ..(nil)
|
||||
{0x1193e, 0x1193e}, // (nil) ..(nil)
|
||||
{0x11943, 0x11943}, // (nil) ..(nil)
|
||||
{0x1193b, 0x1193c}, // Dives Akuru Sign Anusvar..Dives Akuru Sign Candrab
|
||||
{0x1193e, 0x1193e}, // Dives Akuru Virama ..Dives Akuru Virama
|
||||
{0x11943, 0x11943}, // Dives Akuru Sign Nukta ..Dives Akuru Sign Nukta
|
||||
{0x119d4, 0x119d7}, // Nandinagari Vowel Sign U..Nandinagari Vowel Sign V
|
||||
{0x119da, 0x119db}, // Nandinagari Vowel Sign E..Nandinagari Vowel Sign A
|
||||
{0x119e0, 0x119e0}, // Nandinagari Sign Virama ..Nandinagari Sign Virama
|
||||
|
|
@ -313,12 +322,20 @@ public final class WcWidth {
|
|||
{0x11d95, 0x11d95}, // Gunjala Gondi Sign Anusv..Gunjala Gondi Sign Anusv
|
||||
{0x11d97, 0x11d97}, // Gunjala Gondi Virama ..Gunjala Gondi Virama
|
||||
{0x11ef3, 0x11ef4}, // Makasar Vowel Sign I ..Makasar Vowel Sign U
|
||||
{0x11f00, 0x11f01}, // (nil) ..(nil)
|
||||
{0x11f36, 0x11f3a}, // (nil) ..(nil)
|
||||
{0x11f40, 0x11f40}, // (nil) ..(nil)
|
||||
{0x11f42, 0x11f42}, // (nil) ..(nil)
|
||||
{0x13440, 0x13440}, // (nil) ..(nil)
|
||||
{0x13447, 0x13455}, // (nil) ..(nil)
|
||||
{0x16af0, 0x16af4}, // Bassa Vah Combining High..Bassa Vah Combining High
|
||||
{0x16b30, 0x16b36}, // Pahawh Hmong Mark Cim Tu..Pahawh Hmong Mark Cim Ta
|
||||
{0x16f4f, 0x16f4f}, // Miao Sign Consonant Modi..Miao Sign Consonant Modi
|
||||
{0x16f8f, 0x16f92}, // Miao Tone Right ..Miao Tone Below
|
||||
{0x16fe4, 0x16fe4}, // (nil) ..(nil)
|
||||
{0x16fe4, 0x16fe4}, // Khitan Small Script Fill..Khitan Small Script Fill
|
||||
{0x1bc9d, 0x1bc9e}, // Duployan Thick Letter Se..Duployan Double Mark
|
||||
{0x1cf00, 0x1cf2d}, // Znamenny Combining Mark ..Znamenny Combining Mark
|
||||
{0x1cf30, 0x1cf46}, // Znamenny Combining Tonal..Znamenny Priznak Modifie
|
||||
{0x1d167, 0x1d169}, // Musical Symbol Combining..Musical Symbol Combining
|
||||
{0x1d17b, 0x1d182}, // Musical Symbol Combining..Musical Symbol Combining
|
||||
{0x1d185, 0x1d18b}, // Musical Symbol Combining..Musical Symbol Combining
|
||||
|
|
@ -335,15 +352,19 @@ public final class WcWidth {
|
|||
{0x1e01b, 0x1e021}, // Combining Glagolitic Let..Combining Glagolitic Let
|
||||
{0x1e023, 0x1e024}, // Combining Glagolitic Let..Combining Glagolitic Let
|
||||
{0x1e026, 0x1e02a}, // Combining Glagolitic Let..Combining Glagolitic Let
|
||||
{0x1e08f, 0x1e08f}, // (nil) ..(nil)
|
||||
{0x1e130, 0x1e136}, // Nyiakeng Puachue Hmong T..Nyiakeng Puachue Hmong T
|
||||
{0x1e2ae, 0x1e2ae}, // Toto Sign Rising Tone ..Toto Sign Rising Tone
|
||||
{0x1e2ec, 0x1e2ef}, // Wancho Tone Tup ..Wancho Tone Koini
|
||||
{0x1e4ec, 0x1e4ef}, // (nil) ..(nil)
|
||||
{0x1e8d0, 0x1e8d6}, // Mende Kikakui Combining ..Mende Kikakui Combining
|
||||
{0x1e944, 0x1e94a}, // Adlam Alif Lengthener ..Adlam Nukta
|
||||
{0xe0100, 0xe01ef}, // Variation Selector-17 ..Variation Selector-256
|
||||
};
|
||||
|
||||
// https://github.com/jquast/wcwidth/blob/master/wcwidth/table_wide.py
|
||||
// at commit b29897e5a1b403a0e36f7fc991614981cbc42475 (2020-07-14):
|
||||
// from https://github.com/jquast/wcwidth/pull/64
|
||||
// at commit 1b9b6585b0080ea5cb88dc9815796505724793fe (2022-12-16):
|
||||
private static final int[][] WIDE_EASTASIAN = {
|
||||
{0x01100, 0x0115f}, // Hangul Choseong Kiyeok ..Hangul Choseong Filler
|
||||
{0x0231a, 0x0231b}, // Watch ..Hourglass
|
||||
|
|
@ -392,7 +413,7 @@ public final class WcWidth {
|
|||
{0x03190, 0x031e3}, // Ideographic Annotation L..Cjk Stroke Q
|
||||
{0x031f0, 0x0321e}, // Katakana Letter Small Ku..Parenthesized Korean Cha
|
||||
{0x03220, 0x03247}, // Parenthesized Ideograph ..Circled Ideograph Koto
|
||||
{0x03250, 0x04dbf}, // Partnership Sign ..(nil)
|
||||
{0x03250, 0x04dbf}, // Partnership Sign ..Cjk Unified Ideograph-4d
|
||||
{0x04e00, 0x0a48c}, // Cjk Unified Ideograph-4e..Yi Syllable Yyr
|
||||
{0x0a490, 0x0a4c6}, // Yi Radical Qot ..Yi Radical Ke
|
||||
{0x0a960, 0x0a97c}, // Hangul Choseong Tikeut-m..Hangul Choseong Ssangyeo
|
||||
|
|
@ -404,13 +425,18 @@ public final class WcWidth {
|
|||
{0x0fe68, 0x0fe6b}, // Small Reverse Solidus ..Small Commercial At
|
||||
{0x0ff01, 0x0ff60}, // Fullwidth Exclamation Ma..Fullwidth Right White Pa
|
||||
{0x0ffe0, 0x0ffe6}, // Fullwidth Cent Sign ..Fullwidth Won Sign
|
||||
{0x16fe0, 0x16fe4}, // Tangut Iteration Mark ..(nil)
|
||||
{0x16ff0, 0x16ff1}, // (nil) ..(nil)
|
||||
{0x16fe0, 0x16fe4}, // Tangut Iteration Mark ..Khitan Small Script Fill
|
||||
{0x16ff0, 0x16ff1}, // Vietnamese Alternate Rea..Vietnamese Alternate Rea
|
||||
{0x17000, 0x187f7}, // (nil) ..(nil)
|
||||
{0x18800, 0x18cd5}, // Tangut Component-001 ..(nil)
|
||||
{0x18800, 0x18cd5}, // Tangut Component-001 ..Khitan Small Script Char
|
||||
{0x18d00, 0x18d08}, // (nil) ..(nil)
|
||||
{0x1b000, 0x1b11e}, // Katakana Letter Archaic ..Hentaigana Letter N-mu-m
|
||||
{0x1aff0, 0x1aff3}, // Katakana Letter Minnan T..Katakana Letter Minnan T
|
||||
{0x1aff5, 0x1affb}, // Katakana Letter Minnan T..Katakana Letter Minnan N
|
||||
{0x1affd, 0x1affe}, // Katakana Letter Minnan N..Katakana Letter Minnan N
|
||||
{0x1b000, 0x1b122}, // Katakana Letter Archaic ..Katakana Letter Archaic
|
||||
{0x1b132, 0x1b132}, // (nil) ..(nil)
|
||||
{0x1b150, 0x1b152}, // Hiragana Letter Small Wi..Hiragana Letter Small Wo
|
||||
{0x1b155, 0x1b155}, // (nil) ..(nil)
|
||||
{0x1b164, 0x1b167}, // Katakana Letter Small Wi..Katakana Letter Small N
|
||||
{0x1b170, 0x1b2fb}, // Nushu Character-1b170 ..Nushu Character-1b2fb
|
||||
{0x1f004, 0x1f004}, // Mahjong Tile Red Dragon ..Mahjong Tile Red Dragon
|
||||
|
|
@ -443,24 +469,24 @@ public final class WcWidth {
|
|||
{0x1f680, 0x1f6c5}, // Rocket ..Left Luggage
|
||||
{0x1f6cc, 0x1f6cc}, // Sleeping Accommodation ..Sleeping Accommodation
|
||||
{0x1f6d0, 0x1f6d2}, // Place Of Worship ..Shopping Trolley
|
||||
{0x1f6d5, 0x1f6d7}, // Hindu Temple ..(nil)
|
||||
{0x1f6d5, 0x1f6d7}, // Hindu Temple ..Elevator
|
||||
{0x1f6dc, 0x1f6df}, // (nil) ..Ring Buoy
|
||||
{0x1f6eb, 0x1f6ec}, // Airplane Departure ..Airplane Arriving
|
||||
{0x1f6f4, 0x1f6fc}, // Scooter ..(nil)
|
||||
{0x1f6f4, 0x1f6fc}, // Scooter ..Roller Skate
|
||||
{0x1f7e0, 0x1f7eb}, // Large Orange Circle ..Large Brown Square
|
||||
{0x1f90c, 0x1f93a}, // (nil) ..Fencer
|
||||
{0x1f7f0, 0x1f7f0}, // Heavy Equals Sign ..Heavy Equals Sign
|
||||
{0x1f90c, 0x1f93a}, // Pinched Fingers ..Fencer
|
||||
{0x1f93c, 0x1f945}, // Wrestlers ..Goal Net
|
||||
{0x1f947, 0x1f978}, // First Place Medal ..(nil)
|
||||
{0x1f97a, 0x1f9cb}, // Face With Pleading Eyes ..(nil)
|
||||
{0x1f9cd, 0x1f9ff}, // Standing Person ..Nazar Amulet
|
||||
{0x1fa70, 0x1fa74}, // Ballet Shoes ..(nil)
|
||||
{0x1fa78, 0x1fa7a}, // Drop Of Blood ..Stethoscope
|
||||
{0x1fa80, 0x1fa86}, // Yo-yo ..(nil)
|
||||
{0x1fa90, 0x1faa8}, // Ringed Planet ..(nil)
|
||||
{0x1fab0, 0x1fab6}, // (nil) ..(nil)
|
||||
{0x1fac0, 0x1fac2}, // (nil) ..(nil)
|
||||
{0x1fad0, 0x1fad6}, // (nil) ..(nil)
|
||||
{0x1f947, 0x1f9ff}, // First Place Medal ..Nazar Amulet
|
||||
{0x1fa70, 0x1fa7c}, // Ballet Shoes ..Crutch
|
||||
{0x1fa80, 0x1fa88}, // Yo-yo ..(nil)
|
||||
{0x1fa90, 0x1fabd}, // Ringed Planet ..(nil)
|
||||
{0x1fabf, 0x1fac5}, // (nil) ..Person With Crown
|
||||
{0x1face, 0x1fadb}, // (nil) ..(nil)
|
||||
{0x1fae0, 0x1fae8}, // Melting Face ..(nil)
|
||||
{0x1faf0, 0x1faf8}, // Hand With Index Finger A..(nil)
|
||||
{0x20000, 0x2fffd}, // Cjk Unified Ideograph-20..(nil)
|
||||
{0x30000, 0x3fffd}, // (nil) ..(nil)
|
||||
{0x30000, 0x3fffd}, // Cjk Unified Ideograph-30..(nil)
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -512,4 +538,29 @@ public final class WcWidth {
|
|||
return Character.isHighSurrogate(c) ? width(Character.toCodePoint(c, chars[index + 1])) : width(c);
|
||||
}
|
||||
|
||||
/**
|
||||
* The zero width characters count like combining characters in the `chars` array from start
|
||||
* index to end index (exclusive).
|
||||
*/
|
||||
public static int zeroWidthCharsCount(char[] chars, int start, int end) {
|
||||
if (start < 0 || start >= chars.length)
|
||||
return 0;
|
||||
|
||||
int count = 0;
|
||||
for (int i = start; i < end && i < chars.length;) {
|
||||
if (Character.isHighSurrogate(chars[i])) {
|
||||
if (width(Character.toCodePoint(chars[i], chars[i + 1])) <= 0) {
|
||||
count++;
|
||||
}
|
||||
i += 2;
|
||||
} else {
|
||||
if (width(chars[i]) <= 0) {
|
||||
count++;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,9 @@ static int create_subprocess(JNIEnv* env,
|
|||
char** envp,
|
||||
int* pProcessId,
|
||||
jint rows,
|
||||
jint columns)
|
||||
jint columns,
|
||||
jint cell_width,
|
||||
jint cell_height)
|
||||
{
|
||||
int ptm = open("/dev/ptmx", O_RDWR | O_CLOEXEC);
|
||||
if (ptm < 0) return throw_runtime_exception(env, "Cannot open /dev/ptmx");
|
||||
|
|
@ -57,7 +59,7 @@ static int create_subprocess(JNIEnv* env,
|
|||
tcsetattr(ptm, TCSANOW, &tios);
|
||||
|
||||
/** Set initial winsize. */
|
||||
struct winsize sz = { .ws_row = (unsigned short) rows, .ws_col = (unsigned short) columns };
|
||||
struct winsize sz = { .ws_row = (unsigned short) rows, .ws_col = (unsigned short) columns, .ws_xpixel = (unsigned short) (columns * cell_width), .ws_ypixel = (unsigned short) (rows * cell_height)};
|
||||
ioctl(ptm, TIOCSWINSZ, &sz);
|
||||
|
||||
pid_t pid = fork();
|
||||
|
|
@ -121,7 +123,9 @@ JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_createSubprocess(
|
|||
jobjectArray envVars,
|
||||
jintArray processIdArray,
|
||||
jint rows,
|
||||
jint columns)
|
||||
jint columns,
|
||||
jint cell_width,
|
||||
jint cell_height)
|
||||
{
|
||||
jsize size = args ? (*env)->GetArrayLength(env, args) : 0;
|
||||
char** argv = NULL;
|
||||
|
|
@ -156,7 +160,7 @@ JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_createSubprocess(
|
|||
int procId = 0;
|
||||
char const* cmd_cwd = (*env)->GetStringUTFChars(env, cwd, NULL);
|
||||
char const* cmd_utf8 = (*env)->GetStringUTFChars(env, cmd, NULL);
|
||||
int ptm = create_subprocess(env, cmd_utf8, cmd_cwd, argv, envp, &procId, rows, columns);
|
||||
int ptm = create_subprocess(env, cmd_utf8, cmd_cwd, argv, envp, &procId, rows, columns, cell_width, cell_height);
|
||||
(*env)->ReleaseStringUTFChars(env, cmd, cmd_utf8);
|
||||
(*env)->ReleaseStringUTFChars(env, cmd, cmd_cwd);
|
||||
|
||||
|
|
@ -178,9 +182,9 @@ JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_createSubprocess(
|
|||
return ptm;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_setPtyWindowSize(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fd, jint rows, jint cols)
|
||||
JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_setPtyWindowSize(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fd, jint rows, jint cols, jint cell_width, jint cell_height)
|
||||
{
|
||||
struct winsize sz = { .ws_row = (unsigned short) rows, .ws_col = (unsigned short) cols };
|
||||
struct winsize sz = { .ws_row = (unsigned short) rows, .ws_col = (unsigned short) cols, .ws_xpixel = (unsigned short) (cols * cell_width), .ws_ypixel = (unsigned short) (rows * cell_height) };
|
||||
ioctl(fd, TIOCSWINSZ, &sz);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
package com.termux.terminal;
|
||||
|
||||
public class ApcTest extends TerminalTestCase {
|
||||
|
||||
public void testApcConsumed() {
|
||||
// At time of writing this is part of what yazi sends for probing for kitty graphics protocol support:
|
||||
// https://github.com/sxyazi/yazi/blob/0cdaff98d0b3723caff63eebf1974e7907a43a2c/yazi-adapter/src/emulator.rs#L129
|
||||
// This should not result in anything being written to the screen: If kitty graphics protocol support
|
||||
// is implemented it should instead result in an error code on stdin, and if not it should be consumed
|
||||
// silently just as xterm does. See https://sw.kovidgoyal.net/kitty/graphics-protocol/.
|
||||
withTerminalSized(2, 2)
|
||||
.enterString("\033_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\033\\")
|
||||
.assertLinesAre(" ", " ");
|
||||
|
||||
// It is ok for the APC content to be non printable characters:
|
||||
withTerminalSized(12, 2)
|
||||
.enterString("hello \033_some\023\033_\\apc#end\033\\ world")
|
||||
.assertLinesAre("hello world", " ");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package com.termux.terminal;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** "\033[" is the Control Sequence Introducer char sequence (CSI). */
|
||||
public class ControlSequenceIntroducerTest extends TerminalTestCase {
|
||||
|
||||
|
|
@ -62,4 +64,68 @@ public class ControlSequenceIntroducerTest extends TerminalTestCase {
|
|||
assertEquals("y\nz", mTerminal.getScreen().getTranscriptText());
|
||||
}
|
||||
|
||||
public void testReportPixelSize() {
|
||||
int columns = 3;
|
||||
int rows = 3;
|
||||
withTerminalSized(columns, rows);
|
||||
int cellWidth = TerminalTest.INITIAL_CELL_WIDTH_PIXELS;
|
||||
int cellHeight = TerminalTest.INITIAL_CELL_HEIGHT_PIXELS;
|
||||
assertEnteringStringGivesResponse("\033[14t", "\033[4;" + (rows*cellHeight) + ";" + (columns*cellWidth) + "t");
|
||||
assertEnteringStringGivesResponse("\033[16t", "\033[6;" + cellHeight + ";" + cellWidth + "t");
|
||||
columns = 23;
|
||||
rows = 33;
|
||||
resize(columns, rows);
|
||||
assertEnteringStringGivesResponse("\033[14t", "\033[4;" + (rows*cellHeight) + ";" + (columns*cellWidth) + "t");
|
||||
assertEnteringStringGivesResponse("\033[16t", "\033[6;" + cellHeight + ";" + cellWidth + "t");
|
||||
cellWidth = 8;
|
||||
cellHeight = 18;
|
||||
mTerminal.resize(columns, rows, cellWidth, cellHeight);
|
||||
assertEnteringStringGivesResponse("\033[14t", "\033[4;" + (rows*cellHeight) + ";" + (columns*cellWidth) + "t");
|
||||
assertEnteringStringGivesResponse("\033[16t", "\033[6;" + cellHeight + ";" + cellWidth + "t");
|
||||
}
|
||||
|
||||
/**
|
||||
* See <a href="https://sw.kovidgoyal.net/kitty/underlines/">Colored and styled underlines</a>:
|
||||
*
|
||||
* <pre>
|
||||
* <ESC>[4:0m # no underline
|
||||
* <ESC>[4:1m # straight underline
|
||||
* <ESC>[4:2m # double underline
|
||||
* <ESC>[4:3m # curly underline
|
||||
* <ESC>[4:4m # dotted underline
|
||||
* <ESC>[4:5m # dashed underline
|
||||
* <ESC>[4m # straight underline (for backwards compat)
|
||||
* <ESC>[24m # no underline (for backwards compat)
|
||||
* </pre>
|
||||
* <p>
|
||||
* We currently parse the variants, but map them to normal/no underlines as appropriate
|
||||
*/
|
||||
public void testUnderlineVariants() {
|
||||
for (String suffix : List.of("", ":1", ":2", ":3", ":4", ":5")) {
|
||||
for (String stop : List.of("24", "4:0")) {
|
||||
withTerminalSized(3, 3);
|
||||
enterString("\033[4" + suffix + "m").assertLinesAre(" ", " ", " ");
|
||||
assertEquals(TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE, mTerminal.mEffect);
|
||||
enterString("\033[4;1m").assertLinesAre(" ", " ", " ");
|
||||
assertEquals(TextStyle.CHARACTER_ATTRIBUTE_BOLD | TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE, mTerminal.mEffect);
|
||||
enterString("\033[" + stop + "m").assertLinesAre(" ", " ", " ");
|
||||
assertEquals(TextStyle.CHARACTER_ATTRIBUTE_BOLD, mTerminal.mEffect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void testManyParameters() {
|
||||
StringBuilder b = new StringBuilder("\033[");
|
||||
for (int i = 0; i < 30; i++) {
|
||||
b.append("0;");
|
||||
}
|
||||
b.append("4:2");
|
||||
// This clearing of underline should be ignored as the parameters pass the threshold for too many parameters:
|
||||
b.append("4:0m");
|
||||
withTerminalSized(3, 3)
|
||||
.enterString(b.toString())
|
||||
.assertLinesAre(" ", " ", " ");
|
||||
assertEquals(TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE, mTerminal.mEffect);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ public class HistoryTest extends TerminalTestCase {
|
|||
assertLinesAre("777", "888", "999");
|
||||
assertHistoryStartsWith("666", "555");
|
||||
|
||||
mTerminal.resize(cols, 2);
|
||||
resize(cols, 2);
|
||||
assertHistoryStartsWith("777", "666", "555");
|
||||
|
||||
mTerminal.resize(cols, 3);
|
||||
resize(cols, 3);
|
||||
assertHistoryStartsWith("666", "555");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -72,11 +72,11 @@ public class ResizeTest extends TerminalTestCase {
|
|||
enterString("\r\n");
|
||||
}
|
||||
assertLinesAre("998 ", "999 ", " ");
|
||||
mTerminal.resize(cols, 2);
|
||||
resize(cols, 2);
|
||||
assertLinesAre("999 ", " ");
|
||||
mTerminal.resize(cols, 5);
|
||||
resize(cols, 5);
|
||||
assertLinesAre("996 ", "997 ", "998 ", "999 ", " ");
|
||||
mTerminal.resize(cols, rows);
|
||||
resize(cols, rows);
|
||||
assertLinesAre("998 ", "999 ", " ");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -75,6 +75,16 @@ public class ScrollRegionTest extends TerminalTestCase {
|
|||
withTerminalSized(3, 3).enterString("\033[?69h\033[2sABC\033[?6h\033ED").assertLinesAre("ABC", " D ", " ");
|
||||
}
|
||||
|
||||
public void testRiRespectsLeftMargin() {
|
||||
// Reverse Index (RI), ${ESC}M, should respect horizontal margins:
|
||||
withTerminalSized(4, 3).enterString("ABCD\033[?69h\033[2;3s\033[?6h\033M").assertLinesAre("A D", " BC ", " ");
|
||||
}
|
||||
|
||||
public void testSdRespectsLeftMargin() {
|
||||
// Scroll Down (SD), ${CSI}${N}T, should respect horizontal margins:
|
||||
withTerminalSized(4, 3).enterString("ABCD\033[?69h\033[2;3s\033[?6h\033[2T").assertLinesAre("A D", " ", " BC ");
|
||||
}
|
||||
|
||||
public void testBackwardIndex() {
|
||||
// vttest "Menu 11.3.2: VT420 Cursor-Movement Test", test 7.
|
||||
// Without margins:
|
||||
|
|
@ -127,4 +137,31 @@ public class ScrollRegionTest extends TerminalTestCase {
|
|||
" xxx"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* See <a href="https://github.com/termux/termux-packages/issues/12556">reported issue</a>.
|
||||
*/
|
||||
public void testClearingWhenScrollingWithMargins() {
|
||||
int newForeground = 2;
|
||||
int newBackground = 3;
|
||||
int size = 3;
|
||||
TerminalTestCase terminal = withTerminalSized(size, size)
|
||||
// Enable horizontal margin and set left margin to 1:
|
||||
.enterString("\033[?69h\033[2s")
|
||||
// Set foreground and background color:
|
||||
.enterString("\033[" + (30 + newForeground) + ";" + (40 + newBackground) + "m")
|
||||
// Enter newlines to scroll down:
|
||||
.enterString("\r\n\r\n\r\n\r\n\r\n");
|
||||
for (int row = 0; row < size; row++) {
|
||||
for (int col = 0; col < size; col++) {
|
||||
// The first column (outside of the scrolling area, due to us setting a left scroll
|
||||
// margin of 1) should be unmodified, the others should use the current style:
|
||||
int expectedForeground = col == 0 ? TextStyle.COLOR_INDEX_FOREGROUND : newForeground;
|
||||
int expectedBackground = col == 0 ? TextStyle.COLOR_INDEX_BACKGROUND : newBackground;
|
||||
terminal.assertForegroundColorAt(row, col, expectedForeground);
|
||||
terminal.assertBackgroundColorAt(row, col, expectedBackground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ public class TerminalTest extends TerminalTestCase {
|
|||
assertEnteringStringGivesResponse("\033[18t", "\033[8;5;5t");
|
||||
for (int width = 3; width < 12; width++) {
|
||||
for (int height = 3; height < 12; height++) {
|
||||
mTerminal.resize(width, height);
|
||||
resize(width, height);
|
||||
assertEnteringStringGivesResponse("\033[18t", "\033[8;" + height + ";" + width + "t");
|
||||
}
|
||||
}
|
||||
|
|
@ -137,6 +137,11 @@ public class TerminalTest extends TerminalTestCase {
|
|||
}
|
||||
|
||||
public void testSelectGraphics() {
|
||||
selectGraphicsTestRun(';');
|
||||
selectGraphicsTestRun(':');
|
||||
}
|
||||
|
||||
public void selectGraphicsTestRun(char separator) {
|
||||
withTerminalSized(5, 5);
|
||||
enterString("\033[31m");
|
||||
assertEquals(mTerminal.mForeColor, 1);
|
||||
|
|
@ -151,31 +156,63 @@ public class TerminalTest extends TerminalTestCase {
|
|||
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
|
||||
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
|
||||
|
||||
// Test CSI resetting to default if sequence starts with ; or has sequential ;;
|
||||
// Check TerminalEmulator.parseArg()
|
||||
enterString("\033[31m\033[m");
|
||||
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
|
||||
enterString("\033[31m\033[;m".replace(';', separator));
|
||||
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
|
||||
enterString("\033[31m\033[0m");
|
||||
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
|
||||
enterString("\033[31m\033[0;m".replace(';', separator));
|
||||
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
|
||||
enterString("\033[31;;m");
|
||||
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
|
||||
enterString("\033[31::m");
|
||||
assertEquals(1, mTerminal.mForeColor);
|
||||
enterString("\033[31;m");
|
||||
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
|
||||
enterString("\033[31:m");
|
||||
assertEquals(1, mTerminal.mForeColor);
|
||||
enterString("\033[31;;41m");
|
||||
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
|
||||
assertEquals(1, mTerminal.mBackColor);
|
||||
enterString("\033[0m");
|
||||
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
|
||||
|
||||
// 256 colors:
|
||||
enterString("\033[38;5;119m");
|
||||
enterString("\033[38;5;119m".replace(';', separator));
|
||||
assertEquals(119, mTerminal.mForeColor);
|
||||
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
|
||||
enterString("\033[48;5;129m");
|
||||
enterString("\033[48;5;129m".replace(';', separator));
|
||||
assertEquals(119, mTerminal.mForeColor);
|
||||
assertEquals(129, mTerminal.mBackColor);
|
||||
|
||||
// Invalid parameter:
|
||||
enterString("\033[48;8;129m");
|
||||
enterString("\033[48;8;129m".replace(';', separator));
|
||||
assertEquals(119, mTerminal.mForeColor);
|
||||
assertEquals(129, mTerminal.mBackColor);
|
||||
|
||||
// Multiple parameters at once:
|
||||
enterString("\033[38;5;178;48;5;179;m");
|
||||
enterString("\033[38;5;178".replace(';', separator) + ";" + "48;5;179m".replace(';', separator));
|
||||
assertEquals(178, mTerminal.mForeColor);
|
||||
assertEquals(179, mTerminal.mBackColor);
|
||||
|
||||
// Omitted parameter means zero:
|
||||
enterString("\033[38;5;m".replace(';', separator));
|
||||
assertEquals(0, mTerminal.mForeColor);
|
||||
assertEquals(179, mTerminal.mBackColor);
|
||||
enterString("\033[48;5;m".replace(';', separator));
|
||||
assertEquals(0, mTerminal.mForeColor);
|
||||
assertEquals(0, mTerminal.mBackColor);
|
||||
|
||||
// 24 bit colors:
|
||||
enterString(("\033[0m")); // Reset fg and bg colors.
|
||||
enterString("\033[38;2;255;127;2m");
|
||||
enterString("\033[38;2;255;127;2m".replace(';', separator));
|
||||
int expectedForeground = 0xff000000 | (255 << 16) | (127 << 8) | 2;
|
||||
assertEquals(expectedForeground, mTerminal.mForeColor);
|
||||
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
|
||||
enterString("\033[48;2;1;2;254m");
|
||||
enterString("\033[48;2;1;2;254m".replace(';', separator));
|
||||
int expectedBackground = 0xff000000 | (1 << 16) | (2 << 8) | 254;
|
||||
assertEquals(expectedForeground, mTerminal.mForeColor);
|
||||
assertEquals(expectedBackground, mTerminal.mBackColor);
|
||||
|
|
@ -184,14 +221,30 @@ public class TerminalTest extends TerminalTestCase {
|
|||
enterString(("\033[0m")); // Reset fg and bg colors.
|
||||
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
|
||||
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
|
||||
enterString("\033[38;2;255;127;2;48;2;1;2;254m");
|
||||
enterString("\033[38;2;255;127;2".replace(';', separator) + ";" + "48;2;1;2;254m".replace(';', separator));
|
||||
assertEquals(expectedForeground, mTerminal.mForeColor);
|
||||
assertEquals(expectedBackground, mTerminal.mBackColor);
|
||||
|
||||
// 24 bit colors, invalid input:
|
||||
enterString("\033[38;2;300;127;2;48;2;1;300;254m");
|
||||
enterString("\033[38;2;300;127;2;48;2;1;300;254m".replace(';', separator));
|
||||
assertEquals(expectedForeground, mTerminal.mForeColor);
|
||||
assertEquals(expectedBackground, mTerminal.mBackColor);
|
||||
|
||||
// 24 bit colors, omitted parameter means zero:
|
||||
enterString("\033[38;2;255;127;m".replace(';', separator));
|
||||
expectedForeground = 0xff000000 | (255 << 16) | (127 << 8);
|
||||
assertEquals(expectedForeground, mTerminal.mForeColor);
|
||||
assertEquals(expectedBackground, mTerminal.mBackColor);
|
||||
enterString("\033[38;2;123;;77m".replace(';', separator));
|
||||
expectedForeground = 0xff000000 | (123 << 16) | 77;
|
||||
assertEquals(expectedForeground, mTerminal.mForeColor);
|
||||
assertEquals(expectedBackground, mTerminal.mBackColor);
|
||||
|
||||
// 24 bit colors, extra sub-parameters are skipped:
|
||||
expectedForeground = 0xff000000 | (255 << 16) | (127 << 8) | 2;
|
||||
enterString("\033[0;38:2:255:127:2:48:2:1:2:254m");
|
||||
assertEquals(expectedForeground, mTerminal.mForeColor);
|
||||
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
|
||||
}
|
||||
|
||||
public void testBackgroundColorErase() {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,10 @@ import java.util.Set;
|
|||
|
||||
public abstract class TerminalTestCase extends TestCase {
|
||||
|
||||
public static class MockTerminalOutput extends TerminalOutput {
|
||||
public static final int INITIAL_CELL_WIDTH_PIXELS = 13;
|
||||
public static final int INITIAL_CELL_HEIGHT_PIXELS = 15;
|
||||
|
||||
public static class MockTerminalOutput extends TerminalOutput {
|
||||
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
public final List<ChangedTitle> titleChanges = new ArrayList<>();
|
||||
public final List<String> clipboardPuts = new ArrayList<>();
|
||||
|
|
@ -108,7 +111,7 @@ public abstract class TerminalTestCase extends TestCase {
|
|||
|
||||
protected TerminalTestCase withTerminalSized(int columns, int rows) {
|
||||
// The tests aren't currently using the client, so a null client will suffice, a dummy client should be implemented if needed
|
||||
mTerminal = new TerminalEmulator(mOutput, columns, rows, rows * 2, null);
|
||||
mTerminal = new TerminalEmulator(mOutput, columns, rows, INITIAL_CELL_WIDTH_PIXELS, INITIAL_CELL_HEIGHT_PIXELS, rows * 2, null);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
@ -201,7 +204,7 @@ public abstract class TerminalTestCase extends TestCase {
|
|||
}
|
||||
|
||||
public TerminalTestCase resize(int cols, int rows) {
|
||||
mTerminal.resize(cols, rows);
|
||||
mTerminal.resize(cols, rows, INITIAL_CELL_WIDTH_PIXELS, INITIAL_CELL_HEIGHT_PIXELS);
|
||||
assertInvariants();
|
||||
return this;
|
||||
}
|
||||
|
|
@ -301,6 +304,11 @@ public abstract class TerminalTestCase extends TestCase {
|
|||
assertEquals(color, TextStyle.decodeForeColor(style));
|
||||
}
|
||||
|
||||
public void assertBackgroundColorAt(int externalRow, int column, int color) {
|
||||
long style = mTerminal.getScreen().mLines[mTerminal.getScreen().externalToInternalRow(externalRow)].getStyle(column);
|
||||
assertEquals(color, TextStyle.decodeBackColor(style));
|
||||
}
|
||||
|
||||
public TerminalTestCase assertColor(int colorIndex, int expected) {
|
||||
int actual = mTerminal.mColors.mCurrentColors[colorIndex];
|
||||
if (expected != actual) {
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@ apply plugin: 'com.android.library'
|
|||
apply plugin: 'maven-publish'
|
||||
|
||||
android {
|
||||
namespace "com.termux.view"
|
||||
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.annotation:annotation:1.3.0"
|
||||
implementation "androidx.annotation:annotation:1.9.0"
|
||||
api project(":terminal-emulator")
|
||||
}
|
||||
|
||||
|
|
@ -29,12 +30,12 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
}
|
||||
|
||||
task sourceJar(type: Jar) {
|
||||
from android.sourceSets.main.java.srcDirs
|
||||
classifier "sources"
|
||||
archiveClassifier = "sources"
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
|
|
@ -42,7 +43,7 @@ afterEvaluate {
|
|||
publications {
|
||||
// Creates a Maven publication called "release".
|
||||
release(MavenPublication) {
|
||||
from components.release
|
||||
from components.findByName('release')
|
||||
groupId = 'com.termux'
|
||||
artifactId = 'terminal-view'
|
||||
version = '0.118.0'
|
||||
|
|
@ -51,3 +52,4 @@ afterEvaluate {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
<manifest package="com.termux.view">
|
||||
<manifest>
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ public final class TerminalRenderer {
|
|||
mTextPaint.setColor(foreColor);
|
||||
|
||||
// The text alignment is the default Paint.Align.LEFT.
|
||||
canvas.drawText(text, startCharIndex, runWidthChars, left, y - mFontLineSpacingAndAscent, mTextPaint);
|
||||
canvas.drawTextRun(text, startCharIndex, runWidthChars, startCharIndex, runWidthChars, left, y - mFontLineSpacingAndAscent, false, mTextPaint);
|
||||
}
|
||||
|
||||
if (savedMatrix) canvas.restore();
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.termux.view;
|
|||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
|
|
@ -10,6 +11,7 @@ import android.graphics.Typeface;
|
|||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.TextUtils;
|
||||
|
|
@ -19,17 +21,20 @@ import android.view.HapticFeedbackConstants;
|
|||
import android.view.InputDevice;
|
||||
import android.view.KeyCharacterMap;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.accessibility.AccessibilityManager;
|
||||
import android.view.autofill.AutofillManager;
|
||||
import android.view.autofill.AutofillValue;
|
||||
import android.view.inputmethod.BaseInputConnection;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
import android.widget.Scroller;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.termux.terminal.KeyHandler;
|
||||
|
|
@ -81,8 +86,51 @@ public final class TerminalView extends View {
|
|||
/** If non-zero, this is the last unicode code point received if that was a combining character. */
|
||||
int mCombiningAccent;
|
||||
|
||||
/**
|
||||
* The current AutoFill type returned for {@link View#getAutofillType()} by {@link #getAutofillType()}.
|
||||
*
|
||||
* The default is {@link #AUTOFILL_TYPE_NONE} so that AutoFill UI, like toolbar above keyboard
|
||||
* is not shown automatically, like on Activity starts/View create. This value should be updated
|
||||
* to required value, like {@link #AUTOFILL_TYPE_TEXT} before calling
|
||||
* {@link AutofillManager#requestAutofill(View)} so that AutoFill UI shows. The updated value
|
||||
* set will automatically be restored to {@link #AUTOFILL_TYPE_NONE} in
|
||||
* {@link #autofill(AutofillValue)} so that AutoFill UI isn't shown anymore by calling
|
||||
* {@link #resetAutoFill()}.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
private int mAutoFillType = AUTOFILL_TYPE_NONE;
|
||||
|
||||
/**
|
||||
* The current AutoFill type returned for {@link View#getImportantForAutofill()} by
|
||||
* {@link #getImportantForAutofill()}.
|
||||
*
|
||||
* The default is {@link #IMPORTANT_FOR_AUTOFILL_NO} so that view is not considered important
|
||||
* for AutoFill. This value should be updated to required value, like
|
||||
* {@link #IMPORTANT_FOR_AUTOFILL_YES} before calling {@link AutofillManager#requestAutofill(View)}
|
||||
* so that Android and apps consider the view as important for AutoFill to process the request.
|
||||
* The updated value set will automatically be restored to {@link #IMPORTANT_FOR_AUTOFILL_NO} in
|
||||
* {@link #autofill(AutofillValue)} by calling {@link #resetAutoFill()}.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
private int mAutoFillImportance = IMPORTANT_FOR_AUTOFILL_NO;
|
||||
|
||||
/**
|
||||
* The current AutoFill hints returned for {@link View#getAutofillHints()} ()} by {@link #getAutofillHints()} ()}.
|
||||
*
|
||||
* The default is an empty `string[]`. This value should be updated to required value. The
|
||||
* updated value set will automatically be restored an empty `string[]` in
|
||||
* {@link #autofill(AutofillValue)} by calling {@link #resetAutoFill()}.
|
||||
*/
|
||||
private String[] mAutoFillHints = new String[0];
|
||||
|
||||
private final boolean mAccessibilityEnabled;
|
||||
|
||||
/** The {@link KeyEvent} is generated from a virtual keyboard, like manually with the {@link KeyEvent#KeyEvent(int, int)} constructor. */
|
||||
public final static int KEY_EVENT_SOURCE_VIRTUAL_KEYBOARD = KeyCharacterMap.VIRTUAL_KEYBOARD; // -1
|
||||
|
||||
/** The {@link KeyEvent} is generated from a non-physical device, like if 0 value is returned by {@link KeyEvent#getDeviceId()}. */
|
||||
public final static int KEY_EVENT_SOURCE_SOFT_KEYBOARD = 0;
|
||||
|
||||
private static final String LOG_TAG = "TerminalView";
|
||||
|
||||
public TerminalView(Context context, AttributeSet attributes) { // NO_UCD (unused code)
|
||||
|
|
@ -380,7 +428,7 @@ public final class TerminalView extends View {
|
|||
}
|
||||
}
|
||||
|
||||
inputCodePoint(codePoint, ctrlHeld, false);
|
||||
inputCodePoint(KEY_EVENT_SOURCE_SOFT_KEYBOARD, codePoint, ctrlHeld, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -403,19 +451,29 @@ public final class TerminalView extends View {
|
|||
}
|
||||
|
||||
public void onScreenUpdated() {
|
||||
onScreenUpdated(false);
|
||||
}
|
||||
|
||||
public void onScreenUpdated(boolean skipScrolling) {
|
||||
if (mEmulator == null) return;
|
||||
|
||||
int rowsInHistory = mEmulator.getScreen().getActiveTranscriptRows();
|
||||
if (mTopRow < -rowsInHistory) mTopRow = -rowsInHistory;
|
||||
|
||||
boolean skipScrolling = false;
|
||||
if (isSelectingText()) {
|
||||
if (isSelectingText() || mEmulator.isAutoScrollDisabled()) {
|
||||
|
||||
// Do not scroll when selecting text.
|
||||
int rowShift = mEmulator.getScrollCounter();
|
||||
if (-mTopRow + rowShift > rowsInHistory) {
|
||||
// .. unless we're hitting the end of history transcript, in which
|
||||
// case we abort text selection and scroll to end.
|
||||
stopTextSelectionMode();
|
||||
if (isSelectingText())
|
||||
stopTextSelectionMode();
|
||||
|
||||
if (mEmulator.isAutoScrollDisabled()) {
|
||||
mTopRow = -rowsInHistory;
|
||||
skipScrolling = true;
|
||||
}
|
||||
} else {
|
||||
skipScrolling = true;
|
||||
mTopRow -= rowShift;
|
||||
|
|
@ -440,6 +498,14 @@ public final class TerminalView extends View {
|
|||
if (mAccessibilityEnabled) setContentDescription(getText());
|
||||
}
|
||||
|
||||
/** This must be called by the hosting activity in {@link Activity#onContextMenuClosed(Menu)}
|
||||
* when context menu for the {@link TerminalView} is started by
|
||||
* {@link TextSelectionCursorController#ACTION_MORE} is closed. */
|
||||
public void onContextMenuClosed(Menu menu) {
|
||||
// Unset the stored text since it shouldn't be used anymore and should be cleared from memory
|
||||
unsetStoredSelectedText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text size, which in turn sets the number of rows and columns.
|
||||
*
|
||||
|
|
@ -550,11 +616,14 @@ public final class TerminalView extends View {
|
|||
if (action == MotionEvent.ACTION_DOWN) showContextMenu();
|
||||
return true;
|
||||
} else if (event.isButtonPressed(MotionEvent.BUTTON_TERTIARY)) {
|
||||
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clipData = clipboard.getPrimaryClip();
|
||||
ClipboardManager clipboardManager = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clipData = clipboardManager.getPrimaryClip();
|
||||
if (clipData != null) {
|
||||
CharSequence paste = clipData.getItemAt(0).coerceToText(getContext());
|
||||
if (!TextUtils.isEmpty(paste)) mEmulator.paste(paste.toString());
|
||||
ClipData.Item clipItem = clipData.getItemAt(0);
|
||||
if (clipItem != null) {
|
||||
CharSequence text = clipItem.coerceToText(getContext());
|
||||
if (!TextUtils.isEmpty(text)) mEmulator.paste(text.toString());
|
||||
}
|
||||
}
|
||||
} else if (mEmulator.isMouseTrackingActive()) { // BUTTON_PRIMARY.
|
||||
switch (event.getAction()) {
|
||||
|
|
@ -578,6 +647,7 @@ public final class TerminalView extends View {
|
|||
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED)
|
||||
mClient.logInfo(LOG_TAG, "onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")");
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
cancelRequestAutoFill();
|
||||
if (isSelectingText()) {
|
||||
stopTextSelectionMode();
|
||||
return true;
|
||||
|
|
@ -755,7 +825,7 @@ public final class TerminalView extends View {
|
|||
if ((result & KeyCharacterMap.COMBINING_ACCENT) != 0) {
|
||||
// If entered combining accent previously, write it out:
|
||||
if (mCombiningAccent != 0)
|
||||
inputCodePoint(mCombiningAccent, controlDown, leftAltDown);
|
||||
inputCodePoint(event.getDeviceId(), mCombiningAccent, controlDown, leftAltDown);
|
||||
mCombiningAccent = result & KeyCharacterMap.COMBINING_ACCENT_MASK;
|
||||
} else {
|
||||
if (mCombiningAccent != 0) {
|
||||
|
|
@ -763,7 +833,7 @@ public final class TerminalView extends View {
|
|||
if (combinedChar > 0) result = combinedChar;
|
||||
mCombiningAccent = 0;
|
||||
}
|
||||
inputCodePoint(result, controlDown, leftAltDown);
|
||||
inputCodePoint(event.getDeviceId(), result, controlDown, leftAltDown);
|
||||
}
|
||||
|
||||
if (mCombiningAccent != oldCombiningAccent) invalidate();
|
||||
|
|
@ -771,9 +841,9 @@ public final class TerminalView extends View {
|
|||
return true;
|
||||
}
|
||||
|
||||
public void inputCodePoint(int codePoint, boolean controlDownFromEvent, boolean leftAltDownFromEvent) {
|
||||
public void inputCodePoint(int eventSource, int codePoint, boolean controlDownFromEvent, boolean leftAltDownFromEvent) {
|
||||
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) {
|
||||
mClient.logInfo(LOG_TAG, "inputCodePoint(codePoint=" + codePoint + ", controlDownFromEvent=" + controlDownFromEvent + ", leftAltDownFromEvent="
|
||||
mClient.logInfo(LOG_TAG, "inputCodePoint(eventSource=" + eventSource + ", codePoint=" + codePoint + ", controlDownFromEvent=" + controlDownFromEvent + ", leftAltDownFromEvent="
|
||||
+ leftAltDownFromEvent + ")");
|
||||
}
|
||||
|
||||
|
|
@ -813,19 +883,22 @@ public final class TerminalView extends View {
|
|||
}
|
||||
|
||||
if (codePoint > -1) {
|
||||
// Work around bluetooth keyboards sending funny unicode characters instead
|
||||
// of the more normal ones from ASCII that terminal programs expect - the
|
||||
// desire to input the original characters should be low.
|
||||
switch (codePoint) {
|
||||
case 0x02DC: // SMALL TILDE.
|
||||
codePoint = 0x007E; // TILDE (~).
|
||||
break;
|
||||
case 0x02CB: // MODIFIER LETTER GRAVE ACCENT.
|
||||
codePoint = 0x0060; // GRAVE ACCENT (`).
|
||||
break;
|
||||
case 0x02C6: // MODIFIER LETTER CIRCUMFLEX ACCENT.
|
||||
codePoint = 0x005E; // CIRCUMFLEX ACCENT (^).
|
||||
break;
|
||||
// If not virtual or soft keyboard.
|
||||
if (eventSource > KEY_EVENT_SOURCE_SOFT_KEYBOARD) {
|
||||
// Work around bluetooth keyboards sending funny unicode characters instead
|
||||
// of the more normal ones from ASCII that terminal programs expect - the
|
||||
// desire to input the original characters should be low.
|
||||
switch (codePoint) {
|
||||
case 0x02DC: // SMALL TILDE.
|
||||
codePoint = 0x007E; // TILDE (~).
|
||||
break;
|
||||
case 0x02CB: // MODIFIER LETTER GRAVE ACCENT.
|
||||
codePoint = 0x0060; // GRAVE ACCENT (`).
|
||||
break;
|
||||
case 0x02C6: // MODIFIER LETTER CIRCUMFLEX ACCENT.
|
||||
codePoint = 0x005E; // CIRCUMFLEX ACCENT (^).
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If left alt, send escape before the code point to make e.g. Alt+B and Alt+F work in readline:
|
||||
|
|
@ -839,6 +912,9 @@ public final class TerminalView extends View {
|
|||
if (mEmulator != null)
|
||||
mEmulator.setCursorBlinkState(true);
|
||||
|
||||
if (handleKeyCodeAction(keyCode, keyMod))
|
||||
return true;
|
||||
|
||||
TerminalEmulator term = mTermSession.getEmulator();
|
||||
String code = KeyHandler.getCode(keyCode, keyMod, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode());
|
||||
if (code == null) return false;
|
||||
|
|
@ -846,6 +922,26 @@ public final class TerminalView extends View {
|
|||
return true;
|
||||
}
|
||||
|
||||
public boolean handleKeyCodeAction(int keyCode, int keyMod) {
|
||||
boolean shiftDown = (keyMod & KeyHandler.KEYMOD_SHIFT) != 0;
|
||||
|
||||
switch (keyCode) {
|
||||
case KeyEvent.KEYCODE_PAGE_UP:
|
||||
case KeyEvent.KEYCODE_PAGE_DOWN:
|
||||
// shift+page_up and shift+page_down should scroll scrollback history instead of
|
||||
// scrolling command history or changing pages
|
||||
if (shiftDown) {
|
||||
long time = SystemClock.uptimeMillis();
|
||||
MotionEvent motionEvent = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0);
|
||||
doScroll(motionEvent, keyCode == KeyEvent.KEYCODE_PAGE_UP ? -mEmulator.mRows : mEmulator.mRows);
|
||||
motionEvent.recycle();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a key is released in the view.
|
||||
*
|
||||
|
|
@ -893,7 +989,7 @@ public final class TerminalView extends View {
|
|||
int newRows = Math.max(4, (viewHeight - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing);
|
||||
|
||||
if (mEmulator == null || (newColumns != mEmulator.mColumns || newRows != mEmulator.mRows)) {
|
||||
mTermSession.updateSize(newColumns, newRows);
|
||||
mTermSession.updateSize(newColumns, newRows, (int) mRenderer.getFontWidth(), mRenderer.getFontLineSpacing());
|
||||
mEmulator = mTermSession.getEmulator();
|
||||
mClient.onEmulatorSet();
|
||||
|
||||
|
|
@ -971,12 +1067,20 @@ public final class TerminalView extends View {
|
|||
if (value.isText()) {
|
||||
mTermSession.write(value.getTextValue().toString());
|
||||
}
|
||||
|
||||
resetAutoFill();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@Override
|
||||
public int getAutofillType() {
|
||||
return AUTOFILL_TYPE_TEXT;
|
||||
return mAutoFillType;
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@Override
|
||||
public String[] getAutofillHints() {
|
||||
return mAutoFillHints;
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
|
|
@ -985,6 +1089,95 @@ public final class TerminalView extends View {
|
|||
return AutofillValue.forText("");
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@Override
|
||||
public int getImportantForAutofill() {
|
||||
return mAutoFillImportance;
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
private synchronized void resetAutoFill() {
|
||||
// Restore none type so that AutoFill UI isn't shown anymore.
|
||||
mAutoFillType = AUTOFILL_TYPE_NONE;
|
||||
mAutoFillImportance = IMPORTANT_FOR_AUTOFILL_NO;
|
||||
mAutoFillHints = new String[0];
|
||||
}
|
||||
|
||||
public AutofillManager getAutoFillManagerService() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return null;
|
||||
|
||||
try {
|
||||
Context context = getContext();
|
||||
if (context == null) return null;
|
||||
return context.getSystemService(AutofillManager.class);
|
||||
} catch (Exception e) {
|
||||
mClient.logStackTraceWithMessage(LOG_TAG, "Failed to get AutofillManager service", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isAutoFillEnabled() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false;
|
||||
|
||||
try {
|
||||
AutofillManager autofillManager = getAutoFillManagerService();
|
||||
return autofillManager != null && autofillManager.isEnabled();
|
||||
} catch (Exception e) {
|
||||
mClient.logStackTraceWithMessage(LOG_TAG, "Failed to check if Autofill is enabled", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void requestAutoFillUsername() {
|
||||
requestAutoFill(
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? new String[]{View.AUTOFILL_HINT_USERNAME} :
|
||||
null);
|
||||
}
|
||||
|
||||
public synchronized void requestAutoFillPassword() {
|
||||
requestAutoFill(
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? new String[]{View.AUTOFILL_HINT_PASSWORD} :
|
||||
null);
|
||||
}
|
||||
|
||||
public synchronized void requestAutoFill(String[] autoFillHints) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
|
||||
if (autoFillHints == null || autoFillHints.length < 1) return;
|
||||
|
||||
try {
|
||||
AutofillManager autofillManager = getAutoFillManagerService();
|
||||
if (autofillManager != null && autofillManager.isEnabled()) {
|
||||
// Update type that will be returned by `getAutofillType()` so that AutoFill UI is shown.
|
||||
mAutoFillType = AUTOFILL_TYPE_TEXT;
|
||||
// Update importance that will be returned by `getImportantForAutofill()` so that
|
||||
// AutoFill considers the view as important.
|
||||
mAutoFillImportance = IMPORTANT_FOR_AUTOFILL_YES;
|
||||
// Update hints that will be returned by `getAutofillHints()` for which to show AutoFill UI.
|
||||
mAutoFillHints = autoFillHints;
|
||||
autofillManager.requestAutofill(this);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
mClient.logStackTraceWithMessage(LOG_TAG, "Failed to request Autofill", e);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void cancelRequestAutoFill() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
|
||||
if (mAutoFillType == AUTOFILL_TYPE_NONE) return;
|
||||
|
||||
try {
|
||||
AutofillManager autofillManager = getAutoFillManagerService();
|
||||
if (autofillManager != null && autofillManager.isEnabled()) {
|
||||
resetAutoFill();
|
||||
autofillManager.cancel();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
mClient.logStackTraceWithMessage(LOG_TAG, "Failed to cancel Autofill request", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
|
|
@ -1184,6 +1377,25 @@ public final class TerminalView extends View {
|
|||
}
|
||||
}
|
||||
|
||||
/** Get the currently selected text if selecting. */
|
||||
public String getSelectedText() {
|
||||
if (isSelectingText() && mTextSelectionCursorController != null)
|
||||
return mTextSelectionCursorController.getSelectedText();
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Get the selected text stored before "MORE" button was pressed on the context menu. */
|
||||
@Nullable
|
||||
public String getStoredSelectedText() {
|
||||
return mTextSelectionCursorController != null ? mTextSelectionCursorController.getStoredSelectedText() : null;
|
||||
}
|
||||
|
||||
/** Unset the selected text stored before "MORE" button was pressed on the context menu. */
|
||||
public void unsetStoredSelectedText() {
|
||||
if (mTextSelectionCursorController != null) mTextSelectionCursorController.unsetStoredSelectedText();
|
||||
}
|
||||
|
||||
private ActionMode getTextSelectionActionMode() {
|
||||
if (mTextSelectionCursorController != null) {
|
||||
return mTextSelectionCursorController.getActionMode();
|
||||
|
|
@ -1245,6 +1457,7 @@ public final class TerminalView extends View {
|
|||
* Define functions required for long hold toolbar.
|
||||
*/
|
||||
private final Runnable mShowFloatingToolbar = new Runnable() {
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
@Override
|
||||
public void run() {
|
||||
if (getTextSelectionActionMode() != null) {
|
||||
|
|
@ -1253,6 +1466,7 @@ public final class TerminalView extends View {
|
|||
}
|
||||
};
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
private void showFloatingToolbar() {
|
||||
if (getTextSelectionActionMode() != null) {
|
||||
int delay = ViewConfiguration.getDoubleTapTimeout();
|
||||
|
|
@ -1260,6 +1474,7 @@ public final class TerminalView extends View {
|
|||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
void hideFloatingToolbar() {
|
||||
if (getTextSelectionActionMode() != null) {
|
||||
removeCallbacks(mShowFloatingToolbar);
|
||||
|
|
@ -1268,7 +1483,7 @@ public final class TerminalView extends View {
|
|||
}
|
||||
|
||||
public void updateFloatingToolbarVisibility(MotionEvent event) {
|
||||
if (getTextSelectionActionMode() != null) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && getTextSelectionActionMode() != null) {
|
||||
switch (event.getActionMasked()) {
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
hideFloatingToolbar();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License
|
||||
*/
|
||||
package com.termux.view.support;
|
||||
|
||||
import android.util.Log;
|
||||
import android.widget.PopupWindow;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* Implementation of PopupWindow compatibility that can call Gingerbread APIs.
|
||||
* https://chromium.googlesource.com/android_tools/+/HEAD/sdk/extras/android/support/v4/src/gingerbread/android/support/v4/widget/PopupWindowCompatGingerbread.java
|
||||
*/
|
||||
public class PopupWindowCompatGingerbread {
|
||||
|
||||
private static Method sSetWindowLayoutTypeMethod;
|
||||
private static boolean sSetWindowLayoutTypeMethodAttempted;
|
||||
private static Method sGetWindowLayoutTypeMethod;
|
||||
private static boolean sGetWindowLayoutTypeMethodAttempted;
|
||||
|
||||
public static void setWindowLayoutType(PopupWindow popupWindow, int layoutType) {
|
||||
if (!sSetWindowLayoutTypeMethodAttempted) {
|
||||
try {
|
||||
sSetWindowLayoutTypeMethod = PopupWindow.class.getDeclaredMethod(
|
||||
"setWindowLayoutType", int.class);
|
||||
sSetWindowLayoutTypeMethod.setAccessible(true);
|
||||
} catch (Exception e) {
|
||||
// Reflection method fetch failed. Oh well.
|
||||
}
|
||||
sSetWindowLayoutTypeMethodAttempted = true;
|
||||
}
|
||||
if (sSetWindowLayoutTypeMethod != null) {
|
||||
try {
|
||||
sSetWindowLayoutTypeMethod.invoke(popupWindow, layoutType);
|
||||
} catch (Exception e) {
|
||||
// Reflection call failed. Oh well.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static int getWindowLayoutType(PopupWindow popupWindow) {
|
||||
if (!sGetWindowLayoutTypeMethodAttempted) {
|
||||
try {
|
||||
sGetWindowLayoutTypeMethod = PopupWindow.class.getDeclaredMethod(
|
||||
"getWindowLayoutType");
|
||||
sGetWindowLayoutTypeMethod.setAccessible(true);
|
||||
} catch (Exception e) {
|
||||
// Reflection method fetch failed. Oh well.
|
||||
}
|
||||
sGetWindowLayoutTypeMethodAttempted = true;
|
||||
}
|
||||
if (sGetWindowLayoutTypeMethod != null) {
|
||||
try {
|
||||
return (Integer) sGetWindowLayoutTypeMethod.invoke(popupWindow);
|
||||
} catch (Exception e) {
|
||||
// Reflection call failed. Oh well.
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,17 +1,18 @@
|
|||
package com.termux.view.textselection;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.view.ActionMode;
|
||||
import android.view.InputDevice;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.terminal.TerminalBuffer;
|
||||
import com.termux.terminal.WcWidth;
|
||||
import com.termux.view.R;
|
||||
|
|
@ -21,6 +22,7 @@ public class TextSelectionCursorController implements CursorController {
|
|||
|
||||
private final TerminalView terminalView;
|
||||
private final TextSelectionHandleView mStartHandle, mEndHandle;
|
||||
private String mStoredSelectedText;
|
||||
private boolean mIsSelectingText = false;
|
||||
private long mShowStartTime = System.currentTimeMillis();
|
||||
|
||||
|
|
@ -28,9 +30,9 @@ public class TextSelectionCursorController implements CursorController {
|
|||
private int mSelX1 = -1, mSelX2 = -1, mSelY1 = -1, mSelY2 = -1;
|
||||
|
||||
private ActionMode mActionMode;
|
||||
private final int ACTION_COPY = 1;
|
||||
private final int ACTION_PASTE = 2;
|
||||
private final int ACTION_MORE = 3;
|
||||
public final int ACTION_COPY = 1;
|
||||
public final int ACTION_PASTE = 2;
|
||||
public final int ACTION_MORE = 3;
|
||||
|
||||
public TextSelectionCursorController(TerminalView terminalView) {
|
||||
this.terminalView = terminalView;
|
||||
|
|
@ -113,7 +115,7 @@ public class TextSelectionCursorController implements CursorController {
|
|||
|
||||
ClipboardManager clipboard = (ClipboardManager) terminalView.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
menu.add(Menu.NONE, ACTION_COPY, Menu.NONE, R.string.copy_text).setShowAsAction(show);
|
||||
menu.add(Menu.NONE, ACTION_PASTE, Menu.NONE, R.string.paste_text).setEnabled(clipboard.hasPrimaryClip()).setShowAsAction(show);
|
||||
menu.add(Menu.NONE, ACTION_PASTE, Menu.NONE, R.string.paste_text).setEnabled(clipboard != null && clipboard.hasPrimaryClip()).setShowAsAction(show);
|
||||
menu.add(Menu.NONE, ACTION_MORE, Menu.NONE, R.string.text_selection_more);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -132,7 +134,7 @@ public class TextSelectionCursorController implements CursorController {
|
|||
|
||||
switch (item.getItemId()) {
|
||||
case ACTION_COPY:
|
||||
String selectedText = terminalView.mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2).trim();
|
||||
String selectedText = getSelectedText();
|
||||
terminalView.mTermSession.onCopyTextToClipboard(selectedText);
|
||||
terminalView.stopTextSelectionMode();
|
||||
break;
|
||||
|
|
@ -141,7 +143,13 @@ public class TextSelectionCursorController implements CursorController {
|
|||
terminalView.mTermSession.onPasteTextFromClipboard();
|
||||
break;
|
||||
case ACTION_MORE:
|
||||
terminalView.stopTextSelectionMode(); //we stop text selection first, otherwise handles will show above popup
|
||||
// We first store the selected text in case TerminalViewClient needs the
|
||||
// selected text before MORE button was pressed since we are going to
|
||||
// stop selection mode
|
||||
mStoredSelectedText = getSelectedText();
|
||||
// The text selection needs to be stopped before showing context menu,
|
||||
// otherwise handles will show above popup
|
||||
terminalView.stopTextSelectionMode();
|
||||
terminalView.showContextMenu();
|
||||
break;
|
||||
}
|
||||
|
|
@ -155,6 +163,12 @@ public class TextSelectionCursorController implements CursorController {
|
|||
|
||||
};
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
mActionMode = terminalView.startActionMode(callback);
|
||||
return;
|
||||
}
|
||||
|
||||
//noinspection NewApi
|
||||
mActionMode = terminalView.startActionMode(new ActionMode.Callback2() {
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
|
|
@ -183,14 +197,19 @@ public class TextSelectionCursorController implements CursorController {
|
|||
int y1 = Math.round((mSelY1 - 1 - terminalView.getTopRow()) * terminalView.mRenderer.getFontLineSpacing());
|
||||
int y2 = Math.round((mSelY2 + 1 - terminalView.getTopRow()) * terminalView.mRenderer.getFontLineSpacing());
|
||||
|
||||
|
||||
if (x1 > x2) {
|
||||
int tmp = x1;
|
||||
x1 = x2;
|
||||
x2 = tmp;
|
||||
}
|
||||
|
||||
outRect.set(x1, y1 + mHandleHeight, x2, y2 + mHandleHeight);
|
||||
int terminalBottom = terminalView.getBottom();
|
||||
int top = y1 + mHandleHeight;
|
||||
int bottom = y2 + mHandleHeight;
|
||||
if (top > terminalBottom) top = terminalBottom;
|
||||
if (bottom > terminalBottom) bottom = terminalBottom;
|
||||
|
||||
outRect.set(x1, top, x2, bottom);
|
||||
}
|
||||
}, ActionMode.TYPE_FLOATING);
|
||||
}
|
||||
|
|
@ -351,6 +370,22 @@ public class TextSelectionCursorController implements CursorController {
|
|||
sel[3] = mSelX2;
|
||||
}
|
||||
|
||||
/** Get the currently selected text. */
|
||||
public String getSelectedText() {
|
||||
return terminalView.mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2);
|
||||
}
|
||||
|
||||
/** Get the selected text stored before "MORE" button was pressed on the context menu. */
|
||||
@Nullable
|
||||
public String getStoredSelectedText() {
|
||||
return mStoredSelectedText;
|
||||
}
|
||||
|
||||
/** Unset the selected text stored before "MORE" button was pressed on the context menu. */
|
||||
public void unsetStoredSelectedText() {
|
||||
mStoredSelectedText = null;
|
||||
}
|
||||
|
||||
public ActionMode getActionMode() {
|
||||
return mActionMode;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import android.annotation.SuppressLint;
|
|||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.SystemClock;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
|
@ -14,6 +15,7 @@ import android.widget.PopupWindow;
|
|||
|
||||
import com.termux.view.R;
|
||||
import com.termux.view.TerminalView;
|
||||
import com.termux.view.support.PopupWindowCompatGingerbread;
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
public class TextSelectionHandleView extends View {
|
||||
|
|
@ -68,13 +70,18 @@ public class TextSelectionHandleView extends View {
|
|||
android.R.attr.textSelectHandleWindowStyle);
|
||||
mHandle.setSplitTouchEnabled(true);
|
||||
mHandle.setClippingEnabled(false);
|
||||
mHandle.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
|
||||
mHandle.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
mHandle.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
mHandle.setBackgroundDrawable(null);
|
||||
mHandle.setAnimationStyle(0);
|
||||
mHandle.setEnterTransition(null);
|
||||
mHandle.setExitTransition(null);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mHandle.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
|
||||
mHandle.setEnterTransition(null);
|
||||
mHandle.setExitTransition(null);
|
||||
} else {
|
||||
PopupWindowCompatGingerbread.setWindowLayoutType(mHandle, WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
|
||||
}
|
||||
mHandle.setContentView(this);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,55 +1,12 @@
|
|||
The `termux-shared` library is released under [GPLv3 only](https://www.gnu.org/licenses/gpl-3.0.html) license.
|
||||
The `termux-shared` library is released under [MIT](https://opensource.org/licenses/MIT) license.
|
||||
|
||||
### Exceptions
|
||||
|
||||
#### [MIT License](https://opensource.org/licenses/MIT)
|
||||
#### [GPLv3 only](https://www.gnu.org/licenses/gpl-3.0.html)
|
||||
|
||||
- [`src/main/java/com/termux/shared/termux/TermuxConstants.java`](src/main/java/com/termux/shared/termux/TermuxConstants.java).
|
||||
- [`src/main/java/com/termux/shared/settings/properties/TermuxPropertyConstants.java`](src/main/java/com/termux/shared/settings/properties/TermuxPropertyConstants.java).
|
||||
- [`src/main/java/com/termux/shared/termux/*`](src/main/java/com/termux/shared/termux).
|
||||
|
||||
- [`src/main/java/com/termux/shared/activities/*`](src/main/java/com/termux/shared/activities).
|
||||
|
||||
- [`src/main/java/com/termux/shared/crash/CrashHandler.java`](src/main/java/com/termux/shared/crash/CrashHandler.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/data/DataUtils.java`](src/main/java/com/termux/shared/data/DataUtils.java).
|
||||
- [`src/main/java/com/termux/shared/data/IntentUtils.java`](src/main/java/com/termux/shared/data/IntentUtils.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/file/filesystem/FileType.java`](src/main/java/com/termux/shared/file/filesystem/FileType.java).
|
||||
- [`src/main/java/com/termux/shared/file/filesystem/FileTypes.java`](src/main/java/com/termux/shared/file/filesystem/FileTypes.java).
|
||||
- [`src/main/java/com/termux/shared/file/filesystem/NativeDispatcher.java`](src/main/java/com/termux/shared/file/filesystem/NativeDispatcher.java).
|
||||
- [`src/main/java/com/termux/shared/file/tests/FileUtilsTests.java`](src/main/java/com/termux/shared/file/tests/FileUtilsTests.java).
|
||||
- [`src/main/java/com/termux/shared/file/FileUtils.java`](src/main/java/com/termux/shared/file/FileUtils.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/interact/ShareUtils.java`](src/main/java/com/termux/shared/interact/ShareUtils.java).
|
||||
- [`src/main/java/com/termux/shared/interact/MessageDialogUtils.java`](src/main/java/com/termux/shared/interact/MessageDialogUtils.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/logger/Logger.java`](src/main/java/com/termux/shared/logger/Logger.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/markdown/MarkdownUtils.java`](src/main/java/com/termux/shared/markdown/MarkdownUtils.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/models/*`](src/main/java/com/termux/shared/models).
|
||||
|
||||
- [`src/main/java/com/termux/shared/notification/NotificationUtils.java`](src/main/java/com/termux/shared/notification/NotificationUtils.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/settings/preferences/SharedPreferenceUtils.java`](src/main/java/com/termux/shared/settings/preferences/SharedPreferenceUtils.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/settings/properties/SharedPropertiesParser.java`](src/main/java/com/termux/shared/settings/properties/SharedPropertiesParser.java).
|
||||
- [`src/main/java/com/termux/shared/settings/properties/SharedProperties.java`](src/main/java/com/termux/shared/settings/properties/SharedProperties.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/shell/ResultSender.java`](src/main/java/com/termux/shared/shell/ResultSender.java).
|
||||
- [`src/main/java/com/termux/shared/shell/ShellEnvironmentClient.java`](src/main/java/com/termux/shared/shell/ShellEnvironmentClient.java).
|
||||
- [`src/main/java/com/termux/shared/shell/ShellUtils.java`](src/main/java/com/termux/shared/shell/ShellUtils.java).
|
||||
- [`src/main/java/com/termux/shared/shell/TermuxTask.java`](src/main/java/com/termux/shared/shell/TermuxTask.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/termux/AndroidUtils.java`](src/main/java/com/termux/shared/termux/AndroidUtils.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/view/KeyboardUtils.java`](src/main/java/com/termux/shared/view/KeyboardUtils.java).
|
||||
- [`src/main/java/com/termux/shared/view/ViewUtils.java`](src/main/java/com/termux/shared/view/ViewUtils.java).
|
||||
|
||||
- [`src/main/res/drawable/*`](src/main/res/drawable).
|
||||
- [`src/main/res/layout/*`](src/main/res/layout).
|
||||
- [`src/main/res/menu/*`](src/main/res/menu).
|
||||
- [`src/main/res/values/*`](src/main/res/values).
|
||||
The `GPLv3 only` license applies to all files unless specifically specified by a file/directory, like the [`src/main/java/com/termux/shared/termux/TermuxConstants.java`](src/main/java/com/termux/shared/termux/TermuxConstants.java) and [`src/main/java/com/termux/shared/termux/settings/properties/TermuxPropertyConstants.java`](src/main/java/com/termux/shared/termux/settings/properties/TermuxPropertyConstants.java) files are released under the `MIT` license.
|
||||
##
|
||||
|
||||
|
||||
|
|
@ -59,7 +16,7 @@ The `termux-shared` library is released under [GPLv3 only](https://www.gnu.org/l
|
|||
##
|
||||
|
||||
|
||||
#### [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
#### [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
- [`src/main/java/com/termux/shared/shell/StreamGobbler.java`](src/main/java/com/termux/shared/shell/StreamGobbler.java) uses code from [libsuperuser ](https://github.com/Chainfire/libsuperuser).
|
||||
##
|
||||
|
|
|
|||
|
|
@ -2,22 +2,24 @@ apply plugin: 'com.android.library'
|
|||
apply plugin: 'maven-publish'
|
||||
|
||||
android {
|
||||
namespace = "com.termux.shared"
|
||||
|
||||
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
||||
ndkVersion = System.getenv("JITPACK_NDK_VERSION") ?: project.properties.ndkVersion
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||
implementation "androidx.annotation:annotation:1.3.0"
|
||||
implementation "androidx.core:core:1.6.0"
|
||||
implementation 'com.google.android.material:material:1.4.0'
|
||||
implementation "androidx.appcompat:appcompat:1.6.1"
|
||||
implementation "androidx.annotation:annotation:1.9.0"
|
||||
implementation "androidx.core:core:1.13.1"
|
||||
implementation "com.google.android.material:material:1.12.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 "org.lsposed.hiddenapibypass:hiddenapibypass:6.1"
|
||||
|
||||
// Do not increment version higher than 1.0.0-alpha09 since it will break ViewUtils and needs to be looked into
|
||||
// noinspection GradleDependency
|
||||
implementation "androidx.window:window:1.0.0-alpha09"
|
||||
implementation "androidx.window:window:1.1.0"
|
||||
|
||||
// Do not increment version higher than 2.5 or there
|
||||
// will be runtime exceptions on android < 8
|
||||
|
|
@ -25,12 +27,20 @@ android {
|
|||
implementation "commons-io:commons-io:2.5"
|
||||
|
||||
implementation project(":terminal-view")
|
||||
|
||||
implementation "com.termux:termux-am-library:v2.0.0"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
||||
minSdkVersion project.properties.minSdkVersion.toInteger()
|
||||
targetSdkVersion project.properties.targetSdkVersion.toInteger()
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
externalNativeBuild {
|
||||
ndkBuild {
|
||||
cppFlags ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
|
@ -41,20 +51,29 @@ android {
|
|||
}
|
||||
|
||||
compileOptions {
|
||||
// Flag to enable support for the new language APIs
|
||||
coreLibraryDesugaringEnabled true
|
||||
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
ndkBuild {
|
||||
path file('src/main/cpp/Android.mk')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
androidTestImplementation "androidx.test.ext:junit:1.1.5"
|
||||
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.2"
|
||||
}
|
||||
|
||||
task sourceJar(type: Jar) {
|
||||
from android.sourceSets.main.java.srcDirs
|
||||
classifier "sources"
|
||||
archiveClassifier = "sources"
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
|
|
@ -62,7 +81,7 @@ afterEvaluate {
|
|||
publications {
|
||||
// Creates a Maven publication called "release".
|
||||
release(MavenPublication) {
|
||||
from components.release
|
||||
from components.findByName('release')
|
||||
groupId = 'com.termux'
|
||||
artifactId = 'termux-shared'
|
||||
version = '0.118.0'
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue