Compare commits
63 Commits
master
...
github-rel
| Author | SHA1 | Date |
|---|---|---|
|
|
21c0d51fb6 | |
|
|
a8f8be18cb | |
|
|
8f156dc58f | |
|
|
2522c62173 | |
|
|
7f632cf4d2 | |
|
|
999f10d7c7 | |
|
|
3aa986cf7b | |
|
|
b7d2a4b663 | |
|
|
8440a72805 | |
|
|
0cdbe9869e | |
|
|
f84853fc30 | |
|
|
fcc3a4d951 | |
|
|
3f2375029e | |
|
|
e5b578be55 | |
|
|
c09d867b94 | |
|
|
49018f6d91 | |
|
|
0415681d53 | |
|
|
4baf12bae3 | |
|
|
c5f000a162 | |
|
|
c154407624 | |
|
|
254e885fcb | |
|
|
ee9b31a0ee | |
|
|
68865fb148 | |
|
|
bb7fa88fb3 | |
|
|
f35063da74 | |
|
|
043923e674 | |
|
|
2d38a1ca6e | |
|
|
096464d2af | |
|
|
b48f788a94 | |
|
|
915e053a10 | |
|
|
791bf5f88a | |
|
|
fa710c47e9 | |
|
|
5b5e71949b | |
|
|
f3e73880c9 | |
|
|
43317b78c9 | |
|
|
2a008d836e | |
|
|
daa7ca4d43 | |
|
|
2c82a5581f | |
|
|
708281cea2 | |
|
|
eb0cb408a4 | |
|
|
d11c95b996 | |
|
|
9735ae284d | |
|
|
0813e46330 | |
|
|
6ece249c03 | |
|
|
fc8245bba3 | |
|
|
63833d9c2d | |
|
|
c9e2a75e82 | |
|
|
9433f10757 | |
|
|
fbf55fd40c | |
|
|
160ab68e5b | |
|
|
903f2496cb | |
|
|
2dc7381b89 | |
|
|
e55639e41d | |
|
|
087da0b576 | |
|
|
d7f22982a1 | |
|
|
f222315b0f | |
|
|
e11bcfc9a1 | |
|
|
e3a50cbf32 | |
|
|
af5fef4c4a | |
|
|
03e31d190d | |
|
|
d24a04a10d | |
|
|
aee0da49a0 | |
|
|
87c8f3d35a |
|
|
@ -1 +1,2 @@
|
|||
custom: https://termux.dev/donate
|
||||
patreon: termux
|
||||
custom: https://paypal.me/fornwall
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
commit-message:
|
||||
# Prefix all commit messages with "Changed: "
|
||||
prefix: "Changed"
|
||||
|
|
@ -8,23 +8,16 @@ 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@v6
|
||||
uses: actions/checkout@v2
|
||||
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"
|
||||
|
|
@ -41,14 +34,13 @@ jobs:
|
|||
fi
|
||||
|
||||
APK_DIR_PATH="./app/build/outputs/apk/debug"
|
||||
APK_VERSION_TAG="$RELEASE_VERSION_NAME+${{ env.PACKAGE_VARIANT }}-github-debug"
|
||||
APK_VERSION_TAG="$RELEASE_VERSION_NAME+github-debug"
|
||||
APK_BASENAME_PREFIX="termux-app_$APK_VERSION_TAG"
|
||||
|
||||
echo "Building APKs for 'APK_VERSION_TAG' release"
|
||||
echo "Building APKs for '$RELEASE_VERSION_NAME' 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 '$APK_VERSION_TAG' release."
|
||||
exit_on_error "Build failed for '$RELEASE_VERSION_NAME' release."
|
||||
fi
|
||||
|
||||
echo "Validating APKs"
|
||||
|
|
@ -67,18 +59,17 @@ jobs:
|
|||
"${APK_BASENAME_PREFIX}_x86_64.apk" \
|
||||
"${APK_BASENAME_PREFIX}_x86.apk" \
|
||||
> "${APK_BASENAME_PREFIX}_sha256sums"); then
|
||||
exit_on_error "Generate sha25sums failed for '$APK_VERSION_TAG' release."
|
||||
exit_on_error "Generate sha25sums failed for '$RELEASE_VERSION_NAME' release."
|
||||
fi
|
||||
|
||||
echo "Attaching APKs to github release"
|
||||
if ! hub release edit \
|
||||
-m "" \
|
||||
-a "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_universal.apk" \
|
||||
-a "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_arm64-v8a.apk" \
|
||||
-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/${APK_BASENAME_PREFIX}_sha256sums" \
|
||||
"$RELEASE_VERSION_NAME"; then
|
||||
exit_on_error "Attach APKs to release failed for '$APK_VERSION_TAG' release."
|
||||
if ! gh release upload "$RELEASE_VERSION_NAME" \
|
||||
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_universal.apk" \
|
||||
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_arm64-v8a.apk" \
|
||||
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_armeabi-v7a.apk" \
|
||||
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_x86_64.apk" \
|
||||
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_x86.apk" \
|
||||
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_sha256sums" \
|
||||
; then
|
||||
exit_on_error "Attach APKs to release failed for '$RELEASE_VERSION_NAME' release."
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -12,34 +12,16 @@ 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@v6
|
||||
|
||||
- name: Setup java 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- 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/")"
|
||||
|
|
@ -49,7 +31,7 @@ jobs:
|
|||
fi
|
||||
|
||||
APK_DIR_PATH="./app/build/outputs/apk/debug"
|
||||
APK_VERSION_TAG="$RELEASE_VERSION_NAME-${{ env.PACKAGE_VARIANT }}-github-debug" # Note the "-", GITHUB_SHA will already have "+" before it
|
||||
APK_VERSION_TAG="$RELEASE_VERSION_NAME-github-debug" # Note the "-", GITHUB_SHA will already have "+" before it
|
||||
APK_BASENAME_PREFIX="termux-app_$APK_VERSION_TAG"
|
||||
|
||||
# Used by attachment steps later
|
||||
|
|
@ -57,12 +39,11 @@ jobs:
|
|||
echo "APK_VERSION_TAG=$APK_VERSION_TAG" >> $GITHUB_ENV
|
||||
echo "APK_BASENAME_PREFIX=$APK_BASENAME_PREFIX" >> $GITHUB_ENV
|
||||
|
||||
echo "Building APKs for 'APK_VERSION_TAG' build"
|
||||
echo "Building APKs for '$RELEASE_VERSION_NAME' build"
|
||||
export TERMUX_APP_VERSION_NAME="${RELEASE_VERSION_NAME/v/}" # Used by app/build.gradle
|
||||
export TERMUX_APK_VERSION_TAG="$APK_VERSION_TAG" # Used by app/build.gradle
|
||||
export TERMUX_PACKAGE_VARIANT="${{ env.PACKAGE_VARIANT }}" # Used by app/build.gradle
|
||||
if ! ./gradlew assembleDebug; then
|
||||
exit_on_error "Build failed for '$APK_VERSION_TAG' build."
|
||||
exit_on_error "Build failed for '$RELEASE_VERSION_NAME' build."
|
||||
fi
|
||||
|
||||
echo "Validating APKs"
|
||||
|
|
@ -80,12 +61,12 @@ jobs:
|
|||
"${APK_BASENAME_PREFIX}_armeabi-v7a.apk" \
|
||||
"${APK_BASENAME_PREFIX}_x86_64.apk" \
|
||||
"${APK_BASENAME_PREFIX}_x86.apk" \
|
||||
> "${APK_BASENAME_PREFIX}_sha256sums"); then
|
||||
exit_on_error "Generate sha25sums failed for '$APK_VERSION_TAG' release."
|
||||
> sha256sums); then
|
||||
exit_on_error "Generate sha25sums failed for '$RELEASE_VERSION_NAME' release."
|
||||
fi
|
||||
|
||||
- name: Attach universal APK file
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.APK_BASENAME_PREFIX }}_universal
|
||||
path: |
|
||||
|
|
@ -93,7 +74,7 @@ jobs:
|
|||
${{ env.APK_DIR_PATH }}/output-metadata.json
|
||||
|
||||
- name: Attach arm64-v8a APK file
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.APK_BASENAME_PREFIX }}_arm64-v8a
|
||||
path: |
|
||||
|
|
@ -101,7 +82,7 @@ jobs:
|
|||
${{ env.APK_DIR_PATH }}/output-metadata.json
|
||||
|
||||
- name: Attach armeabi-v7a APK file
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.APK_BASENAME_PREFIX }}_armeabi-v7a
|
||||
path: |
|
||||
|
|
@ -109,7 +90,7 @@ jobs:
|
|||
${{ env.APK_DIR_PATH }}/output-metadata.json
|
||||
|
||||
- name: Attach x86_64 APK file
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.APK_BASENAME_PREFIX }}_x86_64
|
||||
path: |
|
||||
|
|
@ -117,7 +98,7 @@ jobs:
|
|||
${{ env.APK_DIR_PATH }}/output-metadata.json
|
||||
|
||||
- name: Attach x86 APK file
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.APK_BASENAME_PREFIX }}_x86
|
||||
path: |
|
||||
|
|
@ -125,9 +106,9 @@ jobs:
|
|||
${{ env.APK_DIR_PATH }}/output-metadata.json
|
||||
|
||||
- name: Attach sha256sums file
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.APK_BASENAME_PREFIX }}_sha256sums
|
||||
name: sha256sums
|
||||
path: |
|
||||
${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_sha256sums
|
||||
${{ env.APK_DIR_PATH }}/sha256sums
|
||||
${{ env.APK_DIR_PATH }}/output-metadata.json
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
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@v6
|
||||
- uses: gradle/actions/wrapper-validation@5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: gradle/wrapper-validation-action@v1
|
||||
|
|
|
|||
|
|
@ -15,12 +15,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup java 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
uses: actions/checkout@v4
|
||||
- name: Execute tests
|
||||
run: |
|
||||
./gradlew test
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ release/
|
|||
.cxx
|
||||
*.zip
|
||||
|
||||
# Crashlytics configuations
|
||||
com_crashlytics_export_strings.xml
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
|
|
@ -24,10 +27,6 @@ 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 2.0](https://www.apache.org/licenses/LICENSE-2.0) license. 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 License 2.0](https://www.apache.org/licenses/LICENSE-2.0). 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,23 +13,22 @@ 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.
|
||||
|
||||
**We are looking for Termux Android application maintainers.**
|
||||
***
|
||||
|
||||
**@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.
|
||||
|
||||
***
|
||||
|
||||
**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)
|
||||
### 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)
|
||||
##
|
||||
|
||||
|
||||
|
|
@ -50,17 +49,13 @@ The core [Termux](https://github.com/termux/termux-app) app comes with the follo
|
|||
|
||||
## Installation
|
||||
|
||||
Latest version is `v0.118.3`.
|
||||
Latest version is `v0.118.1`.
|
||||
|
||||
**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.**
|
||||
Termux can be obtained through various sources listed below for **only** Android `>= 7`. Support was dropped for Android `5` and `6` on [2020-01-01](https://www.reddit.com/r/termux/comments/dnzdbs/end_of_android56_support_on_20200101/) at `v0.83`, old builds are available on [archive.org](https://archive.org/details/termux-repositories-legacy).
|
||||
|
||||
Termux can be obtained through various sources listed below for **only** Android `>= 7` with full support for apps and packages.
|
||||
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.
|
||||
|
||||
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.
|
||||
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).
|
||||
|
||||
|
|
@ -70,64 +65,67 @@ 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 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.**
|
||||
Termux application can be obtained on `Github` either from [`Github Releases`](https://github.com/termux/termux-app/releases) for version `>= 0.118.0` or from [`Github Build`](https://github.com/termux/termux-app/actions/workflows/debug_build.yml) action workflows.
|
||||
|
||||
The APKs for `GitHub Releases` will be listed under `Assets` drop-down of a release. These are automatically attached when a new version is released.
|
||||
The APKs for `Github 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.
|
||||
|
||||
**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.
|
||||
### Google Play Store **(Deprecated)**
|
||||
|
||||
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.
|
||||
**Termux and its plugins are no longer updated on [Google Play Store](https://play.google.com/store/apps/details?id=com.termux) due to [android 10 issues](https://github.com/termux/termux-packages/wiki/Termux-and-Android-10) and have been deprecated.** The last version released for Android `>= 7` was `v0.101`. **It is highly recommended to not install Termux apps from Play Store any more.**
|
||||
|
||||
There are plans for **unpublishing** the Termux app and all its plugins on Play Store soon so that new users cannot install it and for **disabling** the Termux apps with updates so that existing users **cannot continue using outdated versions**. You are encouraged to move to `F-Droid` or `Github` builds as soon as possible.
|
||||
|
||||
You **will not need to buy plugins again** if you bought them on Play Store. All plugins are free on `F-Droid` and `Github`.
|
||||
|
||||
You can backup all your data under `$HOME/` and `$PREFIX/` before changing installation source, and then restore it afterwards, by following instructions at [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux) before the uninstallation.
|
||||
|
||||
There is currently no work being done to solve android `10` issues and *working* updates will not be resumed on Google Play Store any time soon. We will continue targeting sdk `28` for now. So there is not much point in staying on Play Store builds and waiting for updates to be resumed. If for some reason you don't want to move to `F-Droid` or `Github` sources for now, then at least check [Package Management](https://github.com/termux/termux-packages/wiki/Package-Management) to **change your mirror**, otherwise, you will get **`repository is under maintenance or down`** errors when running `apt` or `pkg` commands. After that, it is also **highly advisable** to run `pkg upgrade` command to update all packages to the latest available versions, or at least update `termux-tools` package with `pkg install termux-tools` command.
|
||||
|
||||
Note that by upgrading old packages to latest versions, like that of `python` may break your setups/scripts since they may not be compatible anymore. Moreover, you will not be able to downgrade the package versions since termux repos only keep the latest version and you will have to manually rebuild the old versions of the packages if required as per https://github.com/termux/termux-packages/wiki/Building-packages.
|
||||
|
||||
If you plan on staying on Play Store sources in future as well, then you may want to **disable automatic updates in Play Store** for Termux apps, since if and when updates to disable Termux apps are released, then **you will not be able to downgrade** and **will be forced** to move since apps won't work anymore. Only a way to backup `termux-app` data may be provided. The `termux-tools` [version `>= 0.135`](https://github.com/termux/termux-packages/pull/7493) will also show a banner at the top of the terminal saying `You are likely using a very old version of Termux, probably installed from the Google Play Store.`, you can remove it by running `rm -f /data/data/com.termux/files/usr/etc/motd-playstore` and restarting the app.
|
||||
|
||||
#### Why Disable?
|
||||
|
||||
<details>
|
||||
<summary>Keystore information</summary>
|
||||
<summary></summary>
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
- They should be disabled because deprecated things get removed and are not supported after some time, its the standard practice. It has been many months now since deprecation was announced and updates have not been released on Play Store since after `29 September 2020`.
|
||||
|
||||
- The new versions have lots of **new features and fixes** which you can mostly check out in the Changelog of [`Github Releases`](https://github.com/termux/termux-app/releases) that you may be missing out. Extra detail is usually provided in [commit messages](https://github.com/termux/termux-app/commits/master).
|
||||
|
||||
- Users on old versions are quite often reporting issues in multiple repositories and support forums that were **fixed months ago**, which we then have to deal with. The maintainers of @termux work in their free time, majorly for free, to work on development and provide support and having to re-re-deal with old issues takes away the already limited time from current work and is not possible to continue doing. Play Store page of `termux-app` has been filled with bad reviews of *"broken app"*, even though its clearly mentioned on the page that app is not being updated, yet users don't read and still install and report issues.
|
||||
|
||||
- Asking people to pay for plugins when the `termux-app` at installation time is broken due to repository issues and has bugs is unethical.
|
||||
|
||||
- Old versions don't have proper logging/debugging and crash report support. Reporting bugs without logs or detailed info is not helpful in solving them.
|
||||
|
||||
- It's also easier for us to solve package related issues and provide custom functionality with app updates, which can't be done if users continue using old versions. For example, the [bintray shudown](https://github.com/termux/termux-packages/wiki/Package-Management) causing package install/update failures for new Play Store users is/was not an issue for F-Droid users since it is being shipped with updated bootstrap and repo info, hence no reported issues from new F-Droid users.
|
||||
</details>
|
||||
|
||||
### 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.
|
||||
|
||||
|
|
@ -144,10 +142,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 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)
|
||||
- [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:support@termux.dev)
|
||||
|
||||
### Wikis
|
||||
|
||||
|
|
@ -233,21 +231,7 @@ 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.
|
||||
|
||||
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.**
|
||||
Commit messages **must** use [Conventional Commits](https://www.conventionalcommits.org) specs so that chagelogs can automatically be generated by the [`create-conventional-changelog`](https://github.com/termux/create-conventional-changelog) script, check its repo for further details on the spec. Use the following `types` as `Added: Add foo`, `Added|Fixed: Add foo and fix bar`, `Changed!: Change baz as a breaking change`, etc. You can optionally add a scope as well, like `Fixed(terminal): Some bug`. The space after `:` is necessary.
|
||||
|
||||
- **Added** for new features.
|
||||
- **Changed** for changes in existing functionality.
|
||||
|
|
@ -255,6 +239,11 @@ Commit messages **must** use the [Conventional Commits](https://www.conventional
|
|||
- **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.
|
||||
##
|
||||
|
||||
|
||||
|
|
@ -262,34 +251,6 @@ Commit messages **must** use the [Conventional Commits](https://www.conventional
|
|||
## 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 [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).
|
||||
- You also need to recompile bootstrap zip for the new package name. Check [here](https://github.com/termux/termux-app/issues/1983) and [here](https://github.com/termux/termux-app/issues/2081#issuecomment-865280111) for experimental work on it.
|
||||
- Currently, not all plugins use `TermuxConstants` from `termux-shared` library and have hardcoded `com.termux` values and will need to be manually patched.
|
||||
- If forking termux plugins, check [Forking and Local Development](https://github.com/termux/termux-app/wiki/Termux-Libraries#forking-and-local-development) for info on how to use termux libraries for plugins.
|
||||
##
|
||||
|
||||
|
||||
|
||||
## 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)
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
Check https://termux.dev/security for info on Termux security policies and how to report vulnerabilities.
|
||||
|
|
@ -2,19 +2,7 @@ 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") ?: ""
|
||||
|
|
@ -23,34 +11,31 @@ 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.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.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.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
|
||||
versionName "0.118.0"
|
||||
versionCode 1001
|
||||
versionName "0.118.2"
|
||||
|
||||
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"
|
||||
|
|
@ -79,7 +64,7 @@ android {
|
|||
|
||||
signingConfigs {
|
||||
debug {
|
||||
storeFile file('testkey_untrusted.jks')
|
||||
storeFile file('dev_keystore.jks')
|
||||
keyAlias 'alias'
|
||||
storePassword 'xrj45yWGLbsO7W0v'
|
||||
keyPassword 'xrj45yWGLbsO7W0v'
|
||||
|
|
@ -99,9 +84,6 @@ android {
|
|||
}
|
||||
|
||||
compileOptions {
|
||||
// Flag to enable support for the new language APIs
|
||||
coreLibraryDesugaringEnabled true
|
||||
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
|
@ -112,7 +94,7 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
lint {
|
||||
lintOptions {
|
||||
disable 'ProtectedPermissions'
|
||||
}
|
||||
|
||||
|
|
@ -132,23 +114,19 @@ 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 : project.ext.packageVariant + "-" + "debug") + "_" + (abi ? abi : "universal") + ".apk")
|
||||
outputFileName = new File("termux-app_" + (apkVersionTag ? apkVersionTag : "debug") + "_" + (abi ? abi : "universal") + ".apk")
|
||||
} else if (variant.buildType.name == "release") {
|
||||
def abi = output.getFilter(com.android.build.OutputFile.ABI)
|
||||
outputFileName = new File("termux-app_" + (apkVersionTag ? apkVersionTag : project.ext.packageVariant + "-" + "release") + "_" + (abi ? abi : "universal") + ".apk")
|
||||
outputFileName = new File("termux-app_" + (apkVersionTag ? apkVersionTag : "release") + "_" + (abi ? abi : "universal") + ".apk")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
testImplementation "org.robolectric:robolectric:4.10"
|
||||
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.2"
|
||||
testImplementation "org.robolectric:robolectric:4.4"
|
||||
}
|
||||
|
||||
task versionName {
|
||||
|
|
@ -182,7 +160,7 @@ def downloadBootstrap(String arch, String expectedChecksum, String version) {
|
|||
if (checksum == expectedChecksum) {
|
||||
return
|
||||
} else {
|
||||
logger.quiet("Deleting old local file with wrong hash: " + localUrl + ": expected: " + expectedChecksum + ", actual: " + checksum)
|
||||
logger.quiet("Deleting old local file with wrong hash: " + localUrl)
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
|
|
@ -217,22 +195,11 @@ clean {
|
|||
|
||||
task downloadBootstraps() {
|
||||
doLast {
|
||||
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 + "\"")
|
||||
}
|
||||
def version = "2025.03.28-r1+apt-android-7"
|
||||
downloadBootstrap("aarch64", "c8d702b6f742935001c37cda81b8ac69504a95d5cf28f2899532dd8cd4b057eb", version)
|
||||
downloadBootstrap("arm", "f3bb9d1b32552b34fff41861dbf193ec5ba2848d67d779ac1c7256da6640f85d", version)
|
||||
downloadBootstrap("i686", "36db3e1ac3547f9a174fd763bd9a484fa1a3449cdd81e1cf2408ff0454f839c6", version)
|
||||
downloadBootstrap("x86_64", "1c124ec2396ee70a51b0b0a574f29aa659526aa2b9f558f993b2fb05d1e51855", version)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,3 +10,8 @@
|
|||
-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,6 +1,7 @@
|
|||
<?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">
|
||||
|
|
@ -21,9 +22,7 @@
|
|||
|
||||
<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" />
|
||||
|
|
@ -33,7 +32,6 @@
|
|||
<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" />
|
||||
|
||||
|
|
@ -44,21 +42,16 @@
|
|||
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.TermuxApp.DayNight.DarkActionBar"
|
||||
tools:targetApi="m">
|
||||
android:theme="@style/Theme.Termux">
|
||||
|
||||
<activity
|
||||
android:name=".app.TermuxActivity"
|
||||
android:exported="true"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|keyboard|keyboardHidden|navigation"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|uiMode|keyboard|keyboardHidden|navigation"
|
||||
android:label="@string/application_name"
|
||||
android:launchMode="singleTask"
|
||||
android:resizeableActivity="true"
|
||||
android:theme="@style/Theme.TermuxActivity.DayNight.NoActionBar"
|
||||
tools:targetApi="n">
|
||||
android:resizeableActivity="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
|
|
@ -77,7 +70,6 @@
|
|||
|
||||
<activity-alias
|
||||
android:name=".HomeActivity"
|
||||
android:exported="true"
|
||||
android:targetActivity=".app.TermuxActivity">
|
||||
|
||||
<!-- Launch activity automatically on boot on Android Things devices -->
|
||||
|
|
@ -95,33 +87,27 @@
|
|||
android:label="@string/application_name"
|
||||
android:parentActivityName=".app.TermuxActivity"
|
||||
android:resizeableActivity="true"
|
||||
tools:targetApi="n" />
|
||||
android:theme="@android:style/Theme.Material.Light.DarkActionBar" />
|
||||
|
||||
<activity
|
||||
android:name=".app.activities.SettingsActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/title_activity_termux_settings"
|
||||
android:theme="@style/Theme.TermuxApp.DayNight.NoActionBar" />
|
||||
android:theme="@style/Theme.AppCompat.Light.DarkActionBar" />
|
||||
|
||||
<activity
|
||||
android:name=".shared.activities.ReportActivity"
|
||||
android:theme="@style/Theme.MarkdownViewActivity.DayNight"
|
||||
android:documentLaunchMode="intoExisting" />
|
||||
android:theme="@style/Theme.AppCompat.TermuxReportActivity"
|
||||
android:documentLaunchMode="intoExisting"
|
||||
/>
|
||||
|
||||
<activity
|
||||
android:name=".app.api.file.FileReceiverActivity"
|
||||
android:name=".filepicker.TermuxFileReceiverActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="false"
|
||||
android:label="@string/application_name"
|
||||
android:noHistory="true"
|
||||
android:resizeableActivity="true"
|
||||
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">
|
||||
android:taskAffinity="${TERMUX_PACKAGE_NAME}.filereceiver">
|
||||
|
||||
<!-- Accept multiple file types when sending. -->
|
||||
<intent-filter>
|
||||
|
|
@ -137,13 +123,6 @@
|
|||
<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" />
|
||||
|
|
@ -156,7 +135,8 @@
|
|||
<data android:mimeType="text/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
</activity>
|
||||
|
||||
|
||||
<provider
|
||||
android:name=".filepicker.TermuxDocumentsProvider"
|
||||
|
|
@ -177,21 +157,9 @@
|
|||
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=".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" />
|
||||
<receiver android:name=".shared.activities.ReportActivity$ReportActivityBroadcastReceiver" android:exported="false" />
|
||||
|
||||
|
||||
<service
|
||||
|
|
|
|||
|
|
@ -12,19 +12,18 @@ import android.os.IBinder;
|
|||
import com.termux.R;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.data.IntentUtils;
|
||||
import com.termux.shared.termux.plugins.TermuxPluginUtils;
|
||||
import com.termux.shared.termux.file.TermuxFileUtils;
|
||||
import com.termux.shared.file.TermuxFileUtils;
|
||||
import com.termux.shared.file.filesystem.FileType;
|
||||
import com.termux.shared.errors.Errno;
|
||||
import com.termux.shared.errors.Error;
|
||||
import com.termux.shared.models.errors.Errno;
|
||||
import com.termux.shared.models.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.shared.shell.command.ExecutionCommand;
|
||||
import com.termux.shared.shell.command.ExecutionCommand.Runner;
|
||||
import com.termux.app.utils.PluginUtils;
|
||||
import com.termux.shared.models.ExecutionCommand;
|
||||
|
||||
/**
|
||||
* A service that receives {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent from third party apps and
|
||||
|
|
@ -63,8 +62,6 @@ 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);
|
||||
|
||||
|
|
@ -75,7 +72,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);
|
||||
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return stopService();
|
||||
}
|
||||
|
||||
|
|
@ -103,21 +100,9 @@ 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);
|
||||
|
||||
// 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.inBackground = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false);
|
||||
executionCommand.backgroundCustomLogLevel = IntentUtils.getIntegerExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL, null);
|
||||
executionCommand.sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION);
|
||||
executionCommand.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);
|
||||
|
|
@ -137,10 +122,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 = TermuxPluginUtils.checkIfAllowExternalAppsPolicyIsViolated(this, LOG_TAG);
|
||||
errmsg = PluginUtils.checkIfAllowExternalAppsPolicyIsViolated(this, LOG_TAG);
|
||||
if (errmsg != null) {
|
||||
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true);
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true);
|
||||
return stopService();
|
||||
}
|
||||
|
||||
|
|
@ -150,7 +135,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);
|
||||
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return stopService();
|
||||
}
|
||||
|
||||
|
|
@ -164,7 +149,7 @@ public class RunCommandService extends Service {
|
|||
false);
|
||||
if (error != null) {
|
||||
executionCommand.setStateFailed(error);
|
||||
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return stopService();
|
||||
}
|
||||
|
||||
|
|
@ -185,7 +170,7 @@ public class RunCommandService extends Service {
|
|||
false, true);
|
||||
if (error != null) {
|
||||
executionCommand.setStateFailed(error);
|
||||
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return stopService();
|
||||
}
|
||||
}
|
||||
|
|
@ -210,11 +195,9 @@ 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_RUNNER, executionCommand.runner);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, executionCommand.inBackground);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL, DataUtils.getStringFromInteger(executionCommand.backgroundCustomLogLevel, null));
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION, executionCommand.sessionAction);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_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,6 +1,8 @@
|
|||
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;
|
||||
|
|
@ -9,6 +11,8 @@ 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.Bundle;
|
||||
import android.os.IBinder;
|
||||
|
|
@ -27,46 +31,37 @@ 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.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.packages.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.termux.crash.TermuxCrashUtils;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.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.termux.extrakeys.ExtraKeysView;
|
||||
import com.termux.shared.termux.interact.TextInputDialogUtils;
|
||||
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.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.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* A terminal emulator activity.
|
||||
* <p/>
|
||||
|
|
@ -77,7 +72,7 @@ import java.util.Arrays;
|
|||
* </ul>
|
||||
* about memory leaks.
|
||||
*/
|
||||
public final class TermuxActivity extends AppCompatActivity implements ServiceConnection {
|
||||
public final class TermuxActivity extends Activity implements ServiceConnection {
|
||||
|
||||
/**
|
||||
* The connection to the {@link TermuxService}. Requested in {@link #onCreate(Bundle)} with a call to
|
||||
|
|
@ -101,7 +96,7 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
* The {@link TerminalSessionClient} interface implementation to allow for communication between
|
||||
* {@link TerminalSession} and {@link TermuxActivity}.
|
||||
*/
|
||||
TermuxTerminalSessionActivityClient mTermuxTerminalSessionActivityClient;
|
||||
TermuxTerminalSessionClient mTermuxTerminalSessionClient;
|
||||
|
||||
/**
|
||||
* Termux app shared preferences manager.
|
||||
|
|
@ -109,7 +104,7 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
private TermuxAppSharedPreferences mPreferences;
|
||||
|
||||
/**
|
||||
* Termux app SharedProperties loaded from termux.properties
|
||||
* Termux app shared properties manager, loaded from termux.properties
|
||||
*/
|
||||
private TermuxAppSharedProperties mProperties;
|
||||
|
||||
|
|
@ -128,11 +123,6 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
*/
|
||||
ExtraKeysView mExtraKeysView;
|
||||
|
||||
/**
|
||||
* The client for the {@link #mExtraKeysView}.
|
||||
*/
|
||||
TermuxTerminalExtraKeys mTermuxTerminalExtraKeys;
|
||||
|
||||
/**
|
||||
* The termux sessions list controller.
|
||||
*/
|
||||
|
|
@ -157,14 +147,7 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
/**
|
||||
* If onResume() was called after onCreate().
|
||||
*/
|
||||
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;
|
||||
private boolean isOnResumeAfterOnCreate = false;
|
||||
|
||||
/**
|
||||
* The {@link TermuxActivity} is in an invalid state and must not be run.
|
||||
|
|
@ -173,7 +156,7 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
|
||||
private int mNavBarHeight;
|
||||
|
||||
private float mTerminalToolbarDefaultHeight;
|
||||
private int mTerminalToolbarDefaultHeight;
|
||||
|
||||
|
||||
private static final int CONTEXT_MENU_SELECT_URL_ID = 0;
|
||||
|
|
@ -190,24 +173,24 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
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");
|
||||
mIsOnResumeAfterOnCreate = true;
|
||||
|
||||
if (savedInstanceState != null)
|
||||
mIsActivityRecreated = savedInstanceState.getBoolean(ARG_ACTIVITY_RECREATED, false);
|
||||
Logger.logDebug(LOG_TAG, "onCreate");
|
||||
isOnResumeAfterOnCreate = 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);
|
||||
|
||||
// Delete ReportInfo serialized object files from cache older than 14 days
|
||||
ReportActivity.deleteReportInfoFilesOlderThanXDays(this, 14, false);
|
||||
|
||||
// Load Termux app SharedProperties from disk
|
||||
mProperties = TermuxAppSharedProperties.getProperties();
|
||||
reloadProperties();
|
||||
// Load termux shared properties
|
||||
mProperties = new TermuxAppSharedProperties(this);
|
||||
|
||||
setActivityTheme();
|
||||
|
||||
|
|
@ -241,6 +224,8 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
}
|
||||
|
||||
setDrawerTheme();
|
||||
|
||||
setTermuxTerminalViewAndClients();
|
||||
|
||||
setTerminalToolbarView(savedInstanceState);
|
||||
|
|
@ -253,26 +238,14 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
|
||||
registerForContextMenu(mTerminalView);
|
||||
|
||||
FileReceiverActivity.updateFileReceiverActivityComponentsState(this);
|
||||
// Start the {@link TermuxService} and make it run regardless of who is bound to it
|
||||
Intent serviceIntent = new Intent(this, TermuxService.class);
|
||||
startService(serviceIntent);
|
||||
|
||||
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;
|
||||
}
|
||||
// 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");
|
||||
|
||||
// Send the {@link TermuxConstants#BROADCAST_TERMUX_OPENED} broadcast to notify apps that Termux
|
||||
// app has been opened.
|
||||
|
|
@ -289,8 +262,8 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
|
||||
mIsVisible = true;
|
||||
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.onStart();
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.onStart();
|
||||
|
||||
if (mTermuxTerminalViewClient != null)
|
||||
mTermuxTerminalViewClient.onStart();
|
||||
|
|
@ -309,17 +282,13 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
|
||||
if (mIsInvalidState) return;
|
||||
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.onResume();
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.onResume();
|
||||
|
||||
if (mTermuxTerminalViewClient != null)
|
||||
mTermuxTerminalViewClient.onResume();
|
||||
|
||||
// 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;
|
||||
isOnResumeAfterOnCreate = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -332,15 +301,15 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
|
||||
mIsVisible = false;
|
||||
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.onStop();
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.onStop();
|
||||
|
||||
if (mTermuxTerminalViewClient != null)
|
||||
mTermuxTerminalViewClient.onStop();
|
||||
|
||||
removeTermuxActivityRootViewGlobalLayoutListener();
|
||||
|
||||
unregisterTermuxActivityBroadcastReceiver();
|
||||
unregisterTermuxActivityBroadcastReceiever();
|
||||
getDrawer().closeDrawers();
|
||||
}
|
||||
|
||||
|
|
@ -367,11 +336,8 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
|
||||
Logger.logVerbose(LOG_TAG, "onSaveInstanceState");
|
||||
|
||||
super.onSaveInstanceState(savedInstanceState);
|
||||
saveTerminalToolbarTextInput(savedInstanceState);
|
||||
savedInstanceState.putBoolean(ARG_ACTIVITY_RECREATED, true);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -385,25 +351,24 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
*/
|
||||
@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 (intent != null && intent.getExtras() != null) {
|
||||
launchFailsafe = intent.getExtras().getBoolean(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false);
|
||||
if (bundle != null) {
|
||||
launchFailsafe = bundle.getBoolean(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false);
|
||||
}
|
||||
mTermuxTerminalSessionActivityClient.addNewSession(launchFailsafe, null);
|
||||
mTermuxTerminalSessionClient.addNewSession(launchFailsafe, null);
|
||||
} catch (WindowManager.BadTokenException e) {
|
||||
// Activity finished - ignore.
|
||||
}
|
||||
|
|
@ -413,24 +378,23 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
finishActivityIfNotFinishing();
|
||||
}
|
||||
} else {
|
||||
// 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())) {
|
||||
Intent i = getIntent();
|
||||
if (i != null && Intent.ACTION_RUN.equals(i.getAction())) {
|
||||
// Android 7.1 app shortcut from res/xml/shortcuts.xml.
|
||||
boolean isFailSafe = intent.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false);
|
||||
mTermuxTerminalSessionActivityClient.addNewSession(isFailSafe, null);
|
||||
boolean isFailSafe = i.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false);
|
||||
mTermuxTerminalSessionClient.addNewSession(isFailSafe, null);
|
||||
} else {
|
||||
mTermuxTerminalSessionActivityClient.setCurrentSession(mTermuxTerminalSessionActivityClient.getCurrentStoredSessionOrLast());
|
||||
mTermuxTerminalSessionClient.setCurrentSession(mTermuxTerminalSessionClient.getCurrentStoredSessionOrLast());
|
||||
}
|
||||
}
|
||||
|
||||
// Update the {@link TerminalSession} and {@link TerminalEmulator} clients.
|
||||
mTermuxService.setTermuxTerminalSessionClient(mTermuxTerminalSessionActivityClient);
|
||||
mTermuxService.setTermuxTerminalSessionClient(mTermuxTerminalSessionClient);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
|
||||
Logger.logDebug(LOG_TAG, "onServiceDisconnected");
|
||||
|
||||
// Respect being stopped from the {@link TermuxService} notification action.
|
||||
|
|
@ -441,24 +405,20 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
|
||||
|
||||
|
||||
|
||||
private void reloadProperties() {
|
||||
mProperties.loadTermuxPropertiesFromDisk();
|
||||
|
||||
if (mTermuxTerminalViewClient != null)
|
||||
mTermuxTerminalViewClient.onReloadProperties();
|
||||
private void setActivityTheme() {
|
||||
if (mProperties.isUsingBlackUI()) {
|
||||
this.setTheme(R.style.Theme_Termux_Black);
|
||||
} else {
|
||||
this.setTheme(R.style.Theme_Termux);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
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 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 setMargins() {
|
||||
|
|
@ -483,8 +443,8 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
|
||||
private void setTermuxTerminalViewAndClients() {
|
||||
// Set termux terminal view and session clients
|
||||
mTermuxTerminalSessionActivityClient = new TermuxTerminalSessionActivityClient(this);
|
||||
mTermuxTerminalViewClient = new TermuxTerminalViewClient(this, mTermuxTerminalSessionActivityClient);
|
||||
mTermuxTerminalSessionClient = new TermuxTerminalSessionClient(this);
|
||||
mTermuxTerminalViewClient = new TermuxTerminalViewClient(this, mTermuxTerminalSessionClient);
|
||||
|
||||
// Set termux terminal view
|
||||
mTerminalView = findViewById(R.id.terminal_view);
|
||||
|
|
@ -493,8 +453,8 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
if (mTermuxTerminalViewClient != null)
|
||||
mTermuxTerminalViewClient.onCreate();
|
||||
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.onCreate();
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.onCreate();
|
||||
}
|
||||
|
||||
private void setTermuxSessionsListView() {
|
||||
|
|
@ -508,9 +468,6 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
|
||||
|
||||
private void setTerminalToolbarView(Bundle savedInstanceState) {
|
||||
mTermuxTerminalExtraKeys = new TermuxTerminalExtraKeys(this, mTerminalView,
|
||||
mTermuxTerminalViewClient, mTermuxTerminalSessionActivityClient);
|
||||
|
||||
final ViewPager terminalToolbarViewPager = getTerminalToolbarViewPager();
|
||||
if (mPreferences.shouldShowTerminalToolbar()) terminalToolbarViewPager.setVisibility(View.VISIBLE);
|
||||
|
||||
|
|
@ -532,8 +489,8 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
if (terminalToolbarViewPager == null) return;
|
||||
|
||||
ViewGroup.LayoutParams layoutParams = terminalToolbarViewPager.getLayoutParams();
|
||||
layoutParams.height = Math.round(mTerminalToolbarDefaultHeight *
|
||||
(mTermuxTerminalExtraKeys.getExtraKeysInfo() == null ? 0 : mTermuxTerminalExtraKeys.getExtraKeysInfo().getMatrix().length) *
|
||||
layoutParams.height = (int) Math.round(mTerminalToolbarDefaultHeight *
|
||||
(mProperties.getExtraKeysInfo() == null ? 0 : mProperties.getExtraKeysInfo().getMatrix().length) *
|
||||
mProperties.getTerminalToolbarHeightScaleFactor());
|
||||
terminalToolbarViewPager.setLayoutParams(layoutParams);
|
||||
}
|
||||
|
|
@ -554,7 +511,7 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
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);
|
||||
|
|
@ -566,17 +523,17 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
private void setSettingsButtonView() {
|
||||
ImageButton settingsButton = findViewById(R.id.settings_button);
|
||||
settingsButton.setOnClickListener(v -> {
|
||||
ActivityUtils.startActivity(this, new Intent(this, SettingsActivity.class));
|
||||
startActivity(new Intent(this, SettingsActivity.class));
|
||||
});
|
||||
}
|
||||
|
||||
private void setNewSessionButtonView() {
|
||||
View newSessionButton = findViewById(R.id.new_session_button);
|
||||
newSessionButton.setOnClickListener(v -> mTermuxTerminalSessionActivityClient.addNewSession(false, null));
|
||||
newSessionButton.setOnClickListener(v -> mTermuxTerminalSessionClient.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 -> mTermuxTerminalSessionActivityClient.addNewSession(false, text),
|
||||
R.string.action_new_session_failsafe, text -> mTermuxTerminalSessionActivityClient.addNewSession(true, text),
|
||||
R.string.action_create_named_session_confirm, text -> mTermuxTerminalSessionClient.addNewSession(false, text),
|
||||
R.string.action_new_session_failsafe, text -> mTermuxTerminalSessionClient.addNewSession(true, text),
|
||||
-1, null, null);
|
||||
return true;
|
||||
});
|
||||
|
|
@ -690,10 +647,10 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
toggleKeepScreenOn();
|
||||
return true;
|
||||
case CONTEXT_MENU_HELP_ID:
|
||||
ActivityUtils.startActivity(this, new Intent(this, HelpActivity.class));
|
||||
startActivity(new Intent(this, HelpActivity.class));
|
||||
return true;
|
||||
case CONTEXT_MENU_SETTINGS_ID:
|
||||
ActivityUtils.startActivity(this, new Intent(this, SettingsActivity.class));
|
||||
startActivity(new Intent(this, SettingsActivity.class));
|
||||
return true;
|
||||
case CONTEXT_MENU_REPORT_ID:
|
||||
mTermuxTerminalViewClient.reportIssueFromTranscript();
|
||||
|
|
@ -729,23 +686,21 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
session.reset();
|
||||
showToast(getResources().getString(R.string.msg_terminal_reset), true);
|
||||
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.onResetTerminalSession();
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.onResetTerminalSession();
|
||||
}
|
||||
}
|
||||
|
||||
private void showStylingDialog() {
|
||||
Intent stylingIntent = new Intent();
|
||||
stylingIntent.setClassName(TermuxConstants.TERMUX_STYLING_PACKAGE_NAME, TermuxConstants.TERMUX_STYLING_APP.TERMUX_STYLING_ACTIVITY_NAME);
|
||||
stylingIntent.setClassName(TermuxConstants.TERMUX_STYLING_PACKAGE_NAME, TermuxConstants.TERMUX_STYLING.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) -> ActivityUtils.startActivity(this, 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) -> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(TermuxConstants.TERMUX_STYLING_FDROID_PACKAGE_URL)))).setNegativeButton(android.R.string.cancel, null).show();
|
||||
}
|
||||
}
|
||||
private void toggleKeepScreenOn() {
|
||||
|
|
@ -761,49 +716,25 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* For processes to access shared internal storage (/sdcard) we need this permission.
|
||||
*/
|
||||
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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
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);
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -825,10 +756,6 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
return mExtraKeysView;
|
||||
}
|
||||
|
||||
public TermuxTerminalExtraKeys getTermuxTerminalExtraKeys() {
|
||||
return mTermuxTerminalExtraKeys;
|
||||
}
|
||||
|
||||
public void setExtraKeysView(ExtraKeysView extraKeysView) {
|
||||
mExtraKeysView = extraKeysView;
|
||||
}
|
||||
|
|
@ -842,10 +769,6 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
return (ViewPager) findViewById(R.id.terminal_toolbar_view_pager);
|
||||
}
|
||||
|
||||
public float getTerminalToolbarDefaultHeight() {
|
||||
return mTerminalToolbarDefaultHeight;
|
||||
}
|
||||
|
||||
public boolean isTerminalViewSelected() {
|
||||
return getTerminalToolbarViewPager().getCurrentItem() == 0;
|
||||
}
|
||||
|
|
@ -864,11 +787,7 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
}
|
||||
|
||||
public boolean isOnResumeAfterOnCreate() {
|
||||
return mIsOnResumeAfterOnCreate;
|
||||
}
|
||||
|
||||
public boolean isActivityRecreated() {
|
||||
return mIsActivityRecreated;
|
||||
return isOnResumeAfterOnCreate;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -885,8 +804,8 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
return mTermuxTerminalViewClient;
|
||||
}
|
||||
|
||||
public TermuxTerminalSessionActivityClient getTermuxTerminalSessionClient() {
|
||||
return mTermuxTerminalSessionActivityClient;
|
||||
public TermuxTerminalSessionClient getTermuxTerminalSessionClient() {
|
||||
return mTermuxTerminalSessionClient;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
|
@ -908,27 +827,25 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
|
||||
|
||||
|
||||
public static void updateTermuxActivityStyling(Context context, boolean recreateActivity) {
|
||||
public static void updateTermuxActivityStyling(Context context) {
|
||||
// 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_NOTIFY_APP_CRASH);
|
||||
intentFilter.addAction(TERMUX_ACTIVITY.ACTION_RELOAD_STYLE);
|
||||
intentFilter.addAction(TERMUX_ACTIVITY.ACTION_REQUEST_PERMISSIONS);
|
||||
intentFilter.addAction(TERMUX_ACTIVITY.ACTION_RELOAD_STYLE);
|
||||
|
||||
registerReceiver(mTermuxActivityBroadcastReceiver, intentFilter);
|
||||
}
|
||||
|
||||
private void unregisterTermuxActivityBroadcastReceiver() {
|
||||
private void unregisterTermuxActivityBroadcastReceiever() {
|
||||
unregisterReceiver(mTermuxActivityBroadcastReceiver);
|
||||
}
|
||||
|
||||
private void fixTermuxActivityBroadcastReceiverIntent(Intent intent) {
|
||||
private void fixTermuxActivityBroadcastReceieverIntent(Intent intent) {
|
||||
if (intent == null) return;
|
||||
|
||||
String extraReloadStyle = intent.getStringExtra(TERMUX_ACTIVITY.EXTRA_RELOAD_STYLE);
|
||||
|
|
@ -944,20 +861,17 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
if (intent == null) return;
|
||||
|
||||
if (mIsVisible) {
|
||||
fixTermuxActivityBroadcastReceiverIntent(intent);
|
||||
fixTermuxActivityBroadcastReceieverIntent(intent);
|
||||
|
||||
switch (intent.getAction()) {
|
||||
case TERMUX_ACTIVITY.ACTION_NOTIFY_APP_CRASH:
|
||||
Logger.logDebug(LOG_TAG, "Received intent to notify app crash");
|
||||
TermuxCrashUtils.notifyAppCrashFromCrashLogFile(context, LOG_TAG);
|
||||
case TERMUX_ACTIVITY.ACTION_REQUEST_PERMISSIONS:
|
||||
Logger.logDebug(LOG_TAG, "Received intent to request storage permissions");
|
||||
if (ensureStoragePermissionGranted())
|
||||
TermuxInstaller.setupStorageSymlinks(TermuxActivity.this);
|
||||
return;
|
||||
case TERMUX_ACTIVITY.ACTION_RELOAD_STYLE:
|
||||
Logger.logDebug(LOG_TAG, "Received intent to reload styling");
|
||||
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);
|
||||
reloadActivityStyling();
|
||||
return;
|
||||
default:
|
||||
}
|
||||
|
|
@ -965,43 +879,41 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
|
|||
}
|
||||
}
|
||||
|
||||
private void reloadActivityStyling(boolean recreateActivity) {
|
||||
if (mProperties != null) {
|
||||
reloadProperties();
|
||||
private void reloadActivityStyling() {
|
||||
if (mProperties!= null) {
|
||||
mProperties.loadTermuxPropertiesFromDisk();
|
||||
|
||||
if (mExtraKeysView != null) {
|
||||
mExtraKeysView.setButtonTextAllCaps(mProperties.shouldExtraKeysTextBeAllCaps());
|
||||
mExtraKeysView.reload(mTermuxTerminalExtraKeys.getExtraKeysInfo(), mTerminalToolbarDefaultHeight);
|
||||
mExtraKeysView.reload(mProperties.getExtraKeysInfo());
|
||||
}
|
||||
|
||||
// Update NightMode.APP_NIGHT_MODE
|
||||
TermuxThemeUtils.setAppNightMode(mProperties.getNightMode());
|
||||
}
|
||||
|
||||
setMargins();
|
||||
setTerminalToolbarHeight();
|
||||
|
||||
FileReceiverActivity.updateFileReceiverActivityComponentsState(this);
|
||||
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.onReloadActivityStyling();
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.onReload();
|
||||
|
||||
if (mTermuxTerminalViewClient != null)
|
||||
mTermuxTerminalViewClient.onReloadActivityStyling();
|
||||
mTermuxTerminalViewClient.onReload();
|
||||
|
||||
if (mTermuxService != null)
|
||||
mTermuxService.setTerminalTranscriptRows();
|
||||
|
||||
// To change the activity and drawer theme, activity needs to be recreated.
|
||||
// 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();
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static void startTermuxActivity(@NonNull final Context context) {
|
||||
ActivityUtils.startActivity(context, newInstance(context));
|
||||
context.startActivity(newInstance(context));
|
||||
}
|
||||
|
||||
public static Intent newInstance(@NonNull final Context context) {
|
||||
|
|
|
|||
|
|
@ -1,85 +1,29 @@
|
|||
package com.termux.app;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
|
||||
import com.termux.BuildConfig;
|
||||
import com.termux.shared.errors.Error;
|
||||
import com.termux.shared.crash.TermuxCrashUtils;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
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();
|
||||
|
||||
Context context = getApplicationContext();
|
||||
|
||||
// Set crash handler for the app
|
||||
TermuxCrashUtils.setDefaultCrashHandler(this);
|
||||
TermuxCrashUtils.setCrashHandler(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);
|
||||
}
|
||||
// Set log level for the app
|
||||
setLogLevel();
|
||||
}
|
||||
|
||||
public static void setLogConfig(Context context) {
|
||||
Logger.setDefaultLogTag(TermuxConstants.TERMUX_APP_NAME);
|
||||
|
||||
private void setLogLevel() {
|
||||
// Load the log level from shared preferences and set it to the {@link Logger.CURRENT_LOG_LEVEL}
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(getApplicationContext());
|
||||
if (preferences == null) return;
|
||||
preferences.setLogLevel(null, preferences.getLogLevel());
|
||||
Logger.logDebug("Starting Application");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,24 +4,25 @@ 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.termux.crash.TermuxCrashUtils;
|
||||
import com.termux.shared.termux.file.TermuxFileUtils;
|
||||
import com.termux.shared.file.TermuxFileUtils;
|
||||
import com.termux.shared.interact.MessageDialogUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.errors.Error;
|
||||
import com.termux.shared.android.PackageUtils;
|
||||
import com.termux.shared.models.ExecutionCommand;
|
||||
import com.termux.shared.models.errors.Error;
|
||||
import com.termux.shared.packages.PackageUtils;
|
||||
import com.termux.shared.shell.TermuxShellEnvironmentClient;
|
||||
import com.termux.shared.shell.TermuxTask;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
|
|
@ -73,9 +74,8 @@ 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 (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));
|
||||
if (!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);
|
||||
|
|
@ -86,14 +86,7 @@ final class TermuxInstaller {
|
|||
}
|
||||
|
||||
if (!isFilesDirectoryAccessible) {
|
||||
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));
|
||||
}
|
||||
|
||||
bootstrapErrorMessage = Error.getMinimalErrorString(filesDirectoryAccessibleError) + "\nTERMUX_FILES_DIR: " + MarkdownUtils.getMarkdownCodeForString(TermuxConstants.TERMUX_FILES_DIR_PATH, false);
|
||||
Logger.logError(LOG_TAG, bootstrapErrorMessage);
|
||||
sendBootstrapCrashReportNotification(activity, bootstrapErrorMessage);
|
||||
MessageDialogUtils.showMessage(activity,
|
||||
|
|
@ -104,8 +97,10 @@ 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)) {
|
||||
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.");
|
||||
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.");
|
||||
} else {
|
||||
whenDone.run();
|
||||
return;
|
||||
|
|
@ -195,7 +190,8 @@ final class TermuxInstaller {
|
|||
outStream.write(buffer, 0, readBytes);
|
||||
}
|
||||
if (zipEntryName.startsWith("bin/") || zipEntryName.startsWith("libexec") ||
|
||||
zipEntryName.startsWith("lib/apt/apt-helper") || zipEntryName.startsWith("lib/apt/methods")) {
|
||||
zipEntryName.startsWith("lib/apt/apt-helper") || zipEntryName.startsWith("lib/apt/methods") ||
|
||||
zipEntryName.equals("etc/termux/bootstrap/termux-bootstrap-second-stage.sh")) {
|
||||
//noinspection OctalInteger
|
||||
Os.chmod(targetFile.getAbsolutePath(), 0700);
|
||||
}
|
||||
|
|
@ -216,11 +212,35 @@ final class TermuxInstaller {
|
|||
throw new RuntimeException("Moving termux prefix staging to prefix directory failed");
|
||||
}
|
||||
|
||||
// Run Termux bootstrap second stage.
|
||||
String termuxBootstrapSecondStageFile = TERMUX_PREFIX_DIR_PATH + "/etc/termux/bootstrap/termux-bootstrap-second-stage.sh";
|
||||
if (!FileUtils.fileExists(termuxBootstrapSecondStageFile, false)) {
|
||||
Logger.logInfo(LOG_TAG, "Not running Termux bootstrap second stage since script not found at \"" + termuxBootstrapSecondStageFile + "\" path.");
|
||||
} else {
|
||||
if (!FileUtils.fileExists(TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/bash", true)) {
|
||||
Logger.logInfo(LOG_TAG, "Not running Termux bootstrap second stage since bash not found.");
|
||||
}
|
||||
Logger.logInfo(LOG_TAG, "Running Termux bootstrap second stage.");
|
||||
|
||||
ExecutionCommand executionCommand = new ExecutionCommand(-1,
|
||||
termuxBootstrapSecondStageFile, null, null,
|
||||
null, true, false);
|
||||
executionCommand.commandLabel = "Termux Bootstrap Second Stage Command";
|
||||
executionCommand.backgroundCustomLogLevel = Logger.LOG_LEVEL_NORMAL;
|
||||
TermuxTask termuxTask = TermuxTask.execute(activity, executionCommand, null, new TermuxShellEnvironmentClient(), true);
|
||||
if (termuxTask == null || !executionCommand.isSuccessful() || executionCommand.resultData.exitCode != 0) {
|
||||
// Generate debug report before deleting broken prefix directory to get `stat` info at time of failure.
|
||||
showBootstrapErrorDialog(activity, whenDone, MarkdownUtils.getMarkdownCodeForString(executionCommand.toString(), true));
|
||||
|
||||
// Delete prefix directory as otherwise when app is restarted, the broken prefix directory would be used and logged into.
|
||||
error = FileUtils.deleteFile("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true);
|
||||
if (error != null)
|
||||
Logger.logErrorExtended(LOG_TAG, error.toString());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
@ -264,19 +284,14 @@ final class TermuxInstaller {
|
|||
}
|
||||
|
||||
private static void sendBootstrapCrashReportNotification(Activity activity, String message) {
|
||||
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" +
|
||||
CrashUtils.sendCrashReportNotification(activity, LOG_TAG,
|
||||
"## Bootstrap Error\n\n" + message + "\n\n" +
|
||||
TermuxUtils.getTermuxDebugMarkdownString(activity),
|
||||
true, false, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGES, true);
|
||||
true, 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.");
|
||||
|
||||
|
|
@ -290,21 +305,15 @@ final class TermuxInstaller {
|
|||
if (error != null) {
|
||||
Logger.logErrorAndShowToast(context, LOG_TAG, error.getMessage());
|
||||
Logger.logErrorExtended(LOG_TAG, "Setup Storage Error\n" + error.toString());
|
||||
TermuxCrashUtils.sendCrashReportNotification(context, LOG_TAG, title, null,
|
||||
"## " + title + "\n\n" + Error.getErrorMarkdownString(error),
|
||||
true, false, TermuxUtils.AppInfoMode.TERMUX_PACKAGE, true);
|
||||
CrashUtils.sendCrashReportNotification(context, LOG_TAG, "## Setup Storage Error\n\n" + Error.getErrorMarkdownString(error), true, 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());
|
||||
|
||||
|
|
@ -320,25 +329,9 @@ final class TermuxInstaller {
|
|||
File moviesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
|
||||
Os.symlink(moviesDir.getAbsolutePath(), new File(storageDir, "movies").getAbsolutePath());
|
||||
|
||||
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++) {
|
||||
final File[] dirs = context.getExternalFilesDirs(null);
|
||||
if (dirs != null && dirs.length > 1) {
|
||||
for (int i = 1; i < dirs.length; i++) {
|
||||
File dir = dirs[i];
|
||||
if (dir == null) continue;
|
||||
String symlinkName = "external-" + i;
|
||||
|
|
@ -347,25 +340,11 @@ 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);
|
||||
TermuxCrashUtils.sendCrashReportNotification(context, LOG_TAG, title, null,
|
||||
"## " + title + "\n\n" + Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)),
|
||||
true, false, TermuxUtils.AppInfoMode.TERMUX_PACKAGE, true);
|
||||
CrashUtils.sendCrashReportNotification(context, LOG_TAG, "## Setup Storage Error\n\n" + Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)), true, true);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
|
|
|
|||
|
|
@ -13,12 +13,9 @@ import android.os.ParcelFileDescriptor;
|
|||
import android.provider.MediaStore;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import com.termux.shared.termux.plugins.TermuxPluginUtils;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.app.utils.PluginUtils;
|
||||
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;
|
||||
|
|
@ -35,13 +32,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, "Called without intent data");
|
||||
Logger.logError(LOG_TAG, "termux-open: 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();
|
||||
|
|
@ -55,8 +52,8 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||
break;
|
||||
}
|
||||
|
||||
String scheme = data.getScheme();
|
||||
if (scheme != null && !UriScheme.SCHEME_FILE.equals(scheme)) {
|
||||
final boolean isExternalUrl = data.getScheme() != null && !data.getScheme().equals("file");
|
||||
if (isExternalUrl) {
|
||||
Intent urlIntent = new Intent(intentAction, data);
|
||||
if (intentAction.equals(Intent.ACTION_SEND)) {
|
||||
urlIntent.putExtra(Intent.EXTRA_TEXT, data.toString());
|
||||
|
|
@ -68,21 +65,14 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||
try {
|
||||
context.startActivity(urlIntent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Logger.logError(LOG_TAG, "No app handles the url " + data);
|
||||
Logger.logError(LOG_TAG, "termux-open: 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, "Not a readable file: '" + fileToShare.getAbsolutePath() + "'");
|
||||
Logger.logError(LOG_TAG, "termux-open: Not a readable file: '" + fileToShare.getAbsolutePath() + "'");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -103,8 +93,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||
contentTypeToUse = contentTypeExtra;
|
||||
}
|
||||
|
||||
// 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());
|
||||
Uri uriToShare = Uri.parse("content://" + TermuxConstants.TERMUX_FILE_SHARE_URI_AUTHORITY + fileToShare.getAbsolutePath());
|
||||
|
||||
if (Intent.ACTION_SEND.equals(intentAction)) {
|
||||
sendIntent.putExtra(Intent.EXTRA_STREAM, uriToShare);
|
||||
|
|
@ -120,7 +109,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||
try {
|
||||
context.startActivity(sendIntent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Logger.logError(LOG_TAG, "No app handles the url " + data);
|
||||
Logger.logError(LOG_TAG, "termux-open: No app handles the url " + data);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -202,25 +191,25 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||
File file = new File(uri.getPath());
|
||||
try {
|
||||
String path = file.getCanonicalPath();
|
||||
String callingPackageName = getCallingPackage();
|
||||
Logger.logDebug(LOG_TAG, "Open file request received from " + callingPackageName + " for \"" + path + "\" with mode \"" + mode + "\"");
|
||||
Logger.logDebug(LOG_TAG, "Open file request received 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 TermuxConstants.PROP_ALLOW_EXTERNAL_APPS property to not set to "true", then throw exception
|
||||
String errmsg = TermuxPluginUtils.checkIfAllowExternalAppsPolicyIsViolated(getContext(), LOG_TAG);
|
||||
// If "allow-external-apps" property to not set to "true", then throw exception
|
||||
String errmsg = PluginUtils.checkIfAllowExternalAppsPolicyIsViolated(getContext(), LOG_TAG);
|
||||
if (errmsg != null) {
|
||||
throw new IllegalArgumentException(errmsg);
|
||||
}
|
||||
|
||||
// **DO NOT** allow 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)) {
|
||||
// Do not allow apps with RUN_COMMAND permission to modify termux apps properties files,
|
||||
// including allow-external-apps
|
||||
if (TermuxConstants.TERMUX_PROPERTIES_PRIMARY_FILE_PATH.equals(path) ||
|
||||
TermuxConstants.TERMUX_PROPERTIES_SECONDARY_FILE_PATH.equals(path) ||
|
||||
TermuxConstants.TERMUX_FLOAT_PROPERTIES_PRIMARY_FILE_PATH.equals(path) ||
|
||||
TermuxConstants.TERMUX_FLOAT_PROPERTIES_SECONDARY_FILE_PATH.equals(path)) {
|
||||
mode = "r";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,46 +5,43 @@ 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.event.SystemEventReceiver;
|
||||
import com.termux.app.terminal.TermuxTerminalSessionActivityClient;
|
||||
import com.termux.app.terminal.TermuxTerminalSessionServiceClient;
|
||||
import com.termux.shared.termux.plugins.TermuxPluginUtils;
|
||||
import com.termux.app.settings.properties.TermuxAppSharedProperties;
|
||||
import com.termux.app.terminal.TermuxTerminalSessionClient;
|
||||
import com.termux.app.utils.PluginUtils;
|
||||
import com.termux.shared.data.IntentUtils;
|
||||
import com.termux.shared.net.uri.UriUtils;
|
||||
import com.termux.shared.errors.Errno;
|
||||
import com.termux.shared.models.errors.Errno;
|
||||
import com.termux.shared.shell.ShellUtils;
|
||||
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.shell.TermuxShellEnvironmentClient;
|
||||
import com.termux.shared.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.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.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.shell.TermuxSession;
|
||||
import com.termux.shared.terminal.TermuxTerminalSessionClientBase;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.notification.NotificationUtils;
|
||||
import com.termux.shared.android.PermissionUtils;
|
||||
import com.termux.shared.packages.PermissionUtils;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
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.shared.models.ExecutionCommand;
|
||||
import com.termux.shared.shell.TermuxTask;
|
||||
import com.termux.terminal.TerminalEmulator;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
import com.termux.terminal.TerminalSessionClient;
|
||||
|
|
@ -53,8 +50,8 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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.
|
||||
* 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.
|
||||
|
|
@ -65,7 +62,9 @@ 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 AppShell.AppShellClient, TermuxSession.TermuxSessionClient {
|
||||
public final class TermuxService extends Service implements TermuxTask.TermuxTaskClient, TermuxSession.TermuxSessionClient {
|
||||
|
||||
private static int EXECUTION_ID = 1000;
|
||||
|
||||
/** This service is only bound from inside the same process and never uses IPC. */
|
||||
class LocalBinder extends Binder {
|
||||
|
|
@ -76,27 +75,34 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
|
|||
|
||||
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.
|
||||
*/
|
||||
private TermuxTerminalSessionActivityClient mTermuxTerminalSessionActivityClient;
|
||||
TermuxTerminalSessionClient mTermuxTerminalSessionClient;
|
||||
|
||||
/** The basic implementation of the {@link TerminalSessionClient} interface to be used by {@link TerminalSession}
|
||||
* that does not hold activity references and only a service reference.
|
||||
* that does not hold activity references.
|
||||
*/
|
||||
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;
|
||||
final TermuxTerminalSessionClientBase mTermuxTerminalSessionClientBase = new TermuxTerminalSessionClientBase();
|
||||
|
||||
/** The wake lock and wifi lock are always acquired and released together. */
|
||||
private PowerManager.WakeLock mWakeLock;
|
||||
|
|
@ -105,21 +111,14 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
|
|||
/** 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")
|
||||
|
|
@ -130,11 +129,7 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
|
|||
// Run again in case service is already started and onCreate() is not called
|
||||
runStartForeground();
|
||||
|
||||
String action = null;
|
||||
if (intent != null) {
|
||||
Logger.logVerboseExtended(LOG_TAG, "Intent Received:\n" + IntentUtils.getIntentString(intent));
|
||||
action = intent.getAction();
|
||||
}
|
||||
String action = intent.getAction();
|
||||
|
||||
if (action != null) {
|
||||
switch (action) {
|
||||
|
|
@ -174,11 +169,6 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
|
|||
actionReleaseWakeLock(false);
|
||||
if (!mWantsToStop)
|
||||
killAllTermuxExecutionCommands();
|
||||
|
||||
TermuxShellManager.onAppExit(this);
|
||||
|
||||
SystemEventReceiver.unregisterPackageUpdateEvents(this);
|
||||
|
||||
runStopForeground();
|
||||
}
|
||||
|
||||
|
|
@ -195,7 +185,7 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
|
|||
// 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 (mTermuxTerminalSessionActivityClient != null)
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
unsetTermuxTerminalSessionClient();
|
||||
return false;
|
||||
}
|
||||
|
|
@ -263,36 +253,28 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
|
|||
private synchronized void killAllTermuxExecutionCommands() {
|
||||
boolean processResult;
|
||||
|
||||
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);
|
||||
Logger.logDebug(LOG_TAG, "Killing TermuxSessions=" + mTermuxSessions.size() + ", TermuxTasks=" + mTermuxTasks.size() + ", PendingPluginExecutionCommands=" + mPendingPluginExecutionCommands.size());
|
||||
|
||||
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))) {
|
||||
TermuxPluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
|
||||
PluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -319,8 +301,18 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
|
|||
mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, TermuxConstants.TERMUX_APP_NAME.toLowerCase());
|
||||
mWifiLock.acquire();
|
||||
|
||||
if (!PermissionUtils.checkIfBatteryOptimizationsDisabled(this)) {
|
||||
PermissionUtils.requestDisableBatteryOptimizations(this);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
updateNotification();
|
||||
|
|
@ -362,41 +354,27 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
|
|||
return;
|
||||
}
|
||||
|
||||
ExecutionCommand executionCommand = new ExecutionCommand(TermuxShellManager.getNextShellId());
|
||||
ExecutionCommand executionCommand = new ExecutionCommand(getNextExecutionId());
|
||||
|
||||
executionCommand.executableUri = intent.getData();
|
||||
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;
|
||||
}
|
||||
executionCommand.inBackground = intent.getBooleanExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, false);
|
||||
|
||||
if (executionCommand.executableUri != null) {
|
||||
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.executable = executionCommand.executableUri.getPath();
|
||||
executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, TERMUX_SERVICE.EXTRA_ARGUMENTS, null);
|
||||
if (Runner.APP_SHELL.equalsRunner(executionCommand.runner))
|
||||
if (executionCommand.inBackground)
|
||||
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) {
|
||||
|
|
@ -407,20 +385,13 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
|
|||
executionCommand.resultConfig.resultFilesSuffix = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILES_SUFFIX, null);
|
||||
}
|
||||
|
||||
if (executionCommand.shellCreateMode == null)
|
||||
executionCommand.shellCreateMode = ShellCreateMode.ALWAYS.getMode();
|
||||
|
||||
// Add the execution command to pending plugin execution commands list
|
||||
mShellManager.mPendingPluginExecutionCommands.add(executionCommand);
|
||||
mPendingPluginExecutionCommands.add(executionCommand);
|
||||
|
||||
if (Runner.APP_SHELL.equalsRunner(executionCommand.runner))
|
||||
if (executionCommand.inBackground) {
|
||||
executeTermuxTaskCommand(executionCommand);
|
||||
else if (Runner.TERMINAL_SESSION.equalsRunner(executionCommand.runner))
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -428,84 +399,62 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
|
|||
|
||||
|
||||
|
||||
/** Execute a shell command in background TermuxTask. */
|
||||
/** Execute a shell command in background {@link TermuxTask}. */
|
||||
private void executeTermuxTaskCommand(ExecutionCommand executionCommand) {
|
||||
if (executionCommand == null) return;
|
||||
|
||||
Logger.logDebug(LOG_TAG, "Executing background \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask command");
|
||||
|
||||
// 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);
|
||||
TermuxTask newTermuxTask = createTermuxTask(executionCommand);
|
||||
}
|
||||
|
||||
/** Create a TermuxTask. */
|
||||
/** Create a {@link TermuxTask}. */
|
||||
@Nullable
|
||||
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));
|
||||
public TermuxTask createTermuxTask(String executablePath, String[] arguments, String stdin, String workingDirectory) {
|
||||
return createTermuxTask(new ExecutionCommand(getNextExecutionId(), executablePath, arguments, stdin, workingDirectory, true, false));
|
||||
}
|
||||
|
||||
/** Create a TermuxTask. */
|
||||
/** Create a {@link TermuxTask}. */
|
||||
@Nullable
|
||||
public synchronized AppShell createTermuxTask(ExecutionCommand executionCommand) {
|
||||
public synchronized TermuxTask createTermuxTask(ExecutionCommand executionCommand) {
|
||||
if (executionCommand == null) return null;
|
||||
|
||||
Logger.logDebug(LOG_TAG, "Creating \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask");
|
||||
|
||||
if (!Runner.APP_SHELL.equalsRunner(executionCommand.runner)) {
|
||||
Logger.logDebug(LOG_TAG, "Ignoring wrong runner \"" + executionCommand.runner + "\" command passed to createTermuxTask()");
|
||||
if (!executionCommand.inBackground) {
|
||||
Logger.logDebug(LOG_TAG, "Ignoring a foreground execution command passed to createTermuxTask()");
|
||||
return null;
|
||||
}
|
||||
|
||||
executionCommand.setShellCommandShellEnvironment = true;
|
||||
|
||||
if (Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE)
|
||||
Logger.logVerboseExtended(LOG_TAG, executionCommand.toString());
|
||||
|
||||
AppShell newTermuxTask = AppShell.execute(this, executionCommand, this,
|
||||
new TermuxShellEnvironment(), null,false);
|
||||
TermuxTask newTermuxTask = TermuxTask.execute(this, executionCommand, this, new TermuxShellEnvironmentClient(), 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)
|
||||
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());
|
||||
}
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
else
|
||||
Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
|
||||
return null;
|
||||
}
|
||||
|
||||
mShellManager.mTermuxTasks.add(newTermuxTask);
|
||||
mTermuxTasks.add(newTermuxTask);
|
||||
|
||||
// Remove the execution command from the pending plugin execution commands list since it has
|
||||
// now been processed
|
||||
if (executionCommand.isPluginExecutionCommand)
|
||||
mShellManager.mPendingPluginExecutionCommands.remove(executionCommand);
|
||||
mPendingPluginExecutionCommands.remove(executionCommand);
|
||||
|
||||
updateNotification();
|
||||
|
||||
return newTermuxTask;
|
||||
}
|
||||
|
||||
/** Callback received when a TermuxTask finishes. */
|
||||
/** Callback received when a {@link TermuxTask} finishes. */
|
||||
@Override
|
||||
public void onAppShellExited(final AppShell termuxTask) {
|
||||
public void onTermuxTaskExited(final TermuxTask termuxTask) {
|
||||
mHandler.post(() -> {
|
||||
if (termuxTask != null) {
|
||||
ExecutionCommand executionCommand = termuxTask.getExecutionCommand();
|
||||
|
|
@ -514,9 +463,9 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
|
|||
|
||||
// If the execution command was started for a plugin, then process the results
|
||||
if (executionCommand != null && executionCommand.isPluginExecutionCommand)
|
||||
TermuxPluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
|
||||
PluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
|
||||
|
||||
mShellManager.mTermuxTasks.remove(termuxTask);
|
||||
mTermuxTasks.remove(termuxTask);
|
||||
}
|
||||
|
||||
updateNotification();
|
||||
|
|
@ -533,23 +482,14 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
|
|||
|
||||
Logger.logDebug(LOG_TAG, "Executing foreground \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession command");
|
||||
|
||||
// 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);
|
||||
String sessionName = null;
|
||||
|
||||
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() + "\"");
|
||||
// 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('-', ' ');
|
||||
}
|
||||
|
||||
if (newTermuxSession == null)
|
||||
newTermuxSession = createTermuxSession(executionCommand);
|
||||
TermuxSession newTermuxSession = createTermuxSession(executionCommand, sessionName);
|
||||
if (newTermuxSession == null) return;
|
||||
|
||||
handleSessionAction(DataUtils.getIntFromString(executionCommand.sessionAction,
|
||||
|
|
@ -559,68 +499,57 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
|
|||
|
||||
/**
|
||||
* Create a {@link TermuxSession}.
|
||||
* Currently called by {@link TermuxTerminalSessionActivityClient#addNewSession(boolean, String)} to add a new {@link TermuxSession}.
|
||||
* Currently called by {@link TermuxTerminalSessionClient#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) {
|
||||
ExecutionCommand executionCommand = new ExecutionCommand(TermuxShellManager.getNextShellId(),
|
||||
executablePath, arguments, stdin, workingDirectory, Runner.TERMINAL_SESSION.getName(), isFailSafe);
|
||||
executionCommand.shellName = sessionName;
|
||||
return createTermuxSession(executionCommand);
|
||||
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);
|
||||
}
|
||||
|
||||
/** Create a {@link TermuxSession}. */
|
||||
@Nullable
|
||||
public synchronized TermuxSession createTermuxSession(ExecutionCommand executionCommand) {
|
||||
public synchronized TermuxSession createTermuxSession(ExecutionCommand executionCommand, String sessionName) {
|
||||
if (executionCommand == null) return null;
|
||||
|
||||
Logger.logDebug(LOG_TAG, "Creating \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession");
|
||||
|
||||
if (!Runner.TERMINAL_SESSION.equalsRunner(executionCommand.runner)) {
|
||||
Logger.logDebug(LOG_TAG, "Ignoring wrong runner \"" + executionCommand.runner + "\" command passed to createTermuxSession()");
|
||||
if (executionCommand.inBackground) {
|
||||
Logger.logDebug(LOG_TAG, "Ignoring a background execution 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
|
||||
TermuxSession newTermuxSession = TermuxSession.execute(this, executionCommand, getTermuxTerminalSessionClient(),
|
||||
this, new TermuxShellEnvironment(), null, executionCommand.isPluginExecutionCommand);
|
||||
executionCommand.terminalTranscriptRows = getTerminalTranscriptRows();
|
||||
TermuxSession newTermuxSession = TermuxSession.execute(this, executionCommand, getTermuxTerminalSessionClient(), this, new TermuxShellEnvironmentClient(), sessionName, 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)
|
||||
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());
|
||||
}
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
else
|
||||
Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
|
||||
return null;
|
||||
}
|
||||
|
||||
mShellManager.mTermuxSessions.add(newTermuxSession);
|
||||
mTermuxSessions.add(newTermuxSession);
|
||||
|
||||
// Remove the execution command from the pending plugin execution commands list since it has
|
||||
// now been processed
|
||||
if (executionCommand.isPluginExecutionCommand)
|
||||
mShellManager.mPendingPluginExecutionCommands.remove(executionCommand);
|
||||
mPendingPluginExecutionCommands.remove(executionCommand);
|
||||
|
||||
// Notify {@link TermuxSessionsListViewController} that sessions list has been updated if
|
||||
// activity in is foreground
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.termuxSessionListNotifyUpdated();
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.termuxSessionListNotifyUpdated();
|
||||
|
||||
updateNotification();
|
||||
|
||||
// No need to recreate the activity since it likely just started and theme should already have applied
|
||||
TermuxActivity.updateTermuxActivityStyling(this, false);
|
||||
TermuxActivity.updateTermuxActivityStyling(this);
|
||||
|
||||
return newTermuxSession;
|
||||
}
|
||||
|
|
@ -630,7 +559,7 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
|
|||
int index = getIndexOfSession(sessionToRemove);
|
||||
|
||||
if (index >= 0)
|
||||
mShellManager.mTermuxSessions.get(index).finish();
|
||||
mTermuxSessions.get(index).finish();
|
||||
|
||||
return index;
|
||||
}
|
||||
|
|
@ -645,41 +574,36 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
|
|||
|
||||
// If the execution command was started for a plugin, then process the results
|
||||
if (executionCommand != null && executionCommand.isPluginExecutionCommand)
|
||||
TermuxPluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
|
||||
PluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
|
||||
|
||||
mShellManager.mTermuxSessions.remove(termuxSession);
|
||||
mTermuxSessions.remove(termuxSession);
|
||||
|
||||
// Notify {@link TermuxSessionsListViewController} that sessions list has been updated if
|
||||
// activity in is foreground
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.termuxSessionListNotifyUpdated();
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.termuxSessionListNotifyUpdated();
|
||||
}
|
||||
|
||||
updateNotification();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
/** Get the terminal transcript rows to be used for new {@link TermuxSession}. */
|
||||
public Integer getTerminalTranscriptRows() {
|
||||
if (mTerminalTranscriptRows == null)
|
||||
setTerminalTranscriptRows();
|
||||
return mTerminalTranscriptRows;
|
||||
}
|
||||
|
||||
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 + "\"");
|
||||
|
|
@ -687,8 +611,8 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
|
|||
switch (sessionAction) {
|
||||
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY:
|
||||
setCurrentStoredTerminalSession(newTerminalSession);
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.setCurrentSession(newTerminalSession);
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.setCurrentSession(newTerminalSession);
|
||||
startTermuxActivity();
|
||||
break;
|
||||
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_OPEN_ACTIVITY:
|
||||
|
|
@ -698,8 +622,8 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
|
|||
break;
|
||||
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_DONT_OPEN_ACTIVITY:
|
||||
setCurrentStoredTerminalSession(newTerminalSession);
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.setCurrentSession(newTerminalSession);
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.setCurrentSession(newTerminalSession);
|
||||
break;
|
||||
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_DONT_OPEN_ACTIVITY:
|
||||
if (getTermuxSessionsSize() == 1)
|
||||
|
|
@ -722,8 +646,8 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
|
|||
} else {
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(this);
|
||||
if (preferences == null) return;
|
||||
if (preferences.arePluginErrorNotificationsEnabled(false))
|
||||
Logger.showToast(this, this.getString(R.string.error_display_over_other_apps_permission_not_granted_to_start_terminal), true);
|
||||
if (preferences.arePluginErrorNotificationsEnabled())
|
||||
Logger.showToast(this, this.getString(R.string.error_display_over_other_apps_permission_not_granted), true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -733,35 +657,35 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
|
|||
|
||||
/** 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 #mTermuxTerminalSessionServiceClient}. Once {@link TermuxActivity} bind
|
||||
* so we just return the {@link #mTermuxTerminalSessionClientBase}. Once {@link TermuxActivity} bind
|
||||
* callback is received, it should call {@link #setTermuxTerminalSessionClient} to set the
|
||||
* {@link TermuxService#mTermuxTerminalSessionActivityClient} so that further terminal sessions are directly
|
||||
* passed the {@link TermuxTerminalSessionActivityClient} object which fully implements the
|
||||
* {@link TermuxService#mTermuxTerminalSessionClient} so that further terminal sessions are directly
|
||||
* passed the {@link TermuxTerminalSessionClient} object which fully implements the
|
||||
* {@link TerminalSessionClient} interface.
|
||||
*
|
||||
* @return Returns the {@link TermuxTerminalSessionActivityClient} if {@link TermuxActivity} has bound with
|
||||
* {@link TermuxService}, otherwise {@link TermuxTerminalSessionServiceClient}.
|
||||
* @return Returns the {@link TermuxTerminalSessionClient} if {@link TermuxActivity} has bound with
|
||||
* {@link TermuxService}, otherwise {@link TermuxTerminalSessionClientBase}.
|
||||
*/
|
||||
public synchronized TermuxTerminalSessionClientBase getTermuxTerminalSessionClient() {
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
return mTermuxTerminalSessionActivityClient;
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
return mTermuxTerminalSessionClient;
|
||||
else
|
||||
return mTermuxTerminalSessionServiceClient;
|
||||
return mTermuxTerminalSessionClientBase;
|
||||
}
|
||||
|
||||
/** This should be called when {@link TermuxActivity#onServiceConnected} is called to set the
|
||||
* {@link TermuxService#mTermuxTerminalSessionActivityClient} variable and update the {@link TerminalSession}
|
||||
* and {@link TerminalEmulator} clients in case they were passed {@link TermuxTerminalSessionServiceClient}
|
||||
* {@link TermuxService#mTermuxTerminalSessionClient} variable and update the {@link TerminalSession}
|
||||
* and {@link TerminalEmulator} clients in case they were passed {@link TermuxTerminalSessionClientBase}
|
||||
* earlier.
|
||||
*
|
||||
* @param termuxTerminalSessionActivityClient The {@link TermuxTerminalSessionActivityClient} object that fully
|
||||
* @param termuxTerminalSessionClient The {@link TermuxTerminalSessionClient} object that fully
|
||||
* implements the {@link TerminalSessionClient} interface.
|
||||
*/
|
||||
public synchronized void setTermuxTerminalSessionClient(TermuxTerminalSessionActivityClient termuxTerminalSessionActivityClient) {
|
||||
mTermuxTerminalSessionActivityClient = termuxTerminalSessionActivityClient;
|
||||
public synchronized void setTermuxTerminalSessionClient(TermuxTerminalSessionClient termuxTerminalSessionClient) {
|
||||
mTermuxTerminalSessionClient = termuxTerminalSessionClient;
|
||||
|
||||
for (int i = 0; i < mShellManager.mTermuxSessions.size(); i++)
|
||||
mShellManager.mTermuxSessions.get(i).getTerminalSession().updateTerminalSessionClient(mTermuxTerminalSessionActivityClient);
|
||||
for (int i = 0; i < mTermuxSessions.size(); i++)
|
||||
mTermuxSessions.get(i).getTerminalSession().updateTerminalSessionClient(mTermuxTerminalSessionClient);
|
||||
}
|
||||
|
||||
/** This should be called when {@link TermuxActivity} has been destroyed and in {@link #onUnbind(Intent)}
|
||||
|
|
@ -769,10 +693,10 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
|
|||
* clients do not hold an activity references.
|
||||
*/
|
||||
public synchronized void unsetTermuxTerminalSessionClient() {
|
||||
for (int i = 0; i < mShellManager.mTermuxSessions.size(); i++)
|
||||
mShellManager.mTermuxSessions.get(i).getTerminalSession().updateTerminalSessionClient(mTermuxTerminalSessionServiceClient);
|
||||
for (int i = 0; i < mTermuxSessions.size(); i++)
|
||||
mTermuxSessions.get(i).getTerminalSession().updateTerminalSessionClient(mTermuxTerminalSessionClientBase);
|
||||
|
||||
mTermuxTerminalSessionActivityClient = null;
|
||||
mTermuxTerminalSessionClient = null;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -789,7 +713,7 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
|
|||
|
||||
// Set notification text
|
||||
int sessionCount = getTermuxSessionsSize();
|
||||
int taskCount = mShellManager.mTermuxTasks.size();
|
||||
int taskCount = mTermuxTasks.size();
|
||||
String notificationText = sessionCount + " session" + (sessionCount == 1 ? "" : "s");
|
||||
if (taskCount > 0) {
|
||||
notificationText += ", " + taskCount + " task" + (taskCount == 1 ? "" : "s");
|
||||
|
|
@ -850,7 +774,7 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
|
|||
|
||||
/** Update the shown foreground service notification after making any changes that affect it. */
|
||||
private synchronized void updateNotification() {
|
||||
if (mWakeLock == null && mShellManager.mTermuxSessions.isEmpty() && mShellManager.mTermuxTasks.isEmpty()) {
|
||||
if (mWakeLock == null && mTermuxSessions.isEmpty() && mTermuxTasks.isEmpty()) {
|
||||
// Exit if we are updating after the user disabled all locks with no sessions or tasks running.
|
||||
requestStopService();
|
||||
} else {
|
||||
|
|
@ -862,55 +786,41 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
|
|||
|
||||
|
||||
|
||||
private void setCurrentStoredTerminalSession(TerminalSession terminalSession) {
|
||||
if (terminalSession == null) return;
|
||||
private void setCurrentStoredTerminalSession(TerminalSession session) {
|
||||
if (session == null) return;
|
||||
// Make the newly created session the current one to be displayed
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(this);
|
||||
if (preferences == null) return;
|
||||
preferences.setCurrentSession(terminalSession.mHandle);
|
||||
preferences.setCurrentSession(session.mHandle);
|
||||
}
|
||||
|
||||
public synchronized boolean isTermuxSessionsEmpty() {
|
||||
return mShellManager.mTermuxSessions.isEmpty();
|
||||
return mTermuxSessions.isEmpty();
|
||||
}
|
||||
|
||||
public synchronized int getTermuxSessionsSize() {
|
||||
return mShellManager.mTermuxSessions.size();
|
||||
return mTermuxSessions.size();
|
||||
}
|
||||
|
||||
public synchronized List<TermuxSession> getTermuxSessions() {
|
||||
return mShellManager.mTermuxSessions;
|
||||
return mTermuxSessions;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public synchronized TermuxSession getTermuxSession(int index) {
|
||||
if (index >= 0 && index < mShellManager.mTermuxSessions.size())
|
||||
return mShellManager.mTermuxSessions.get(index);
|
||||
if (index >= 0 && index < mTermuxSessions.size())
|
||||
return 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 mShellManager.mTermuxSessions.isEmpty() ? null : mShellManager.mTermuxSessions.get(mShellManager.mTermuxSessions.size() - 1);
|
||||
return mTermuxSessions.isEmpty() ? null : mTermuxSessions.get(mTermuxSessions.size() - 1);
|
||||
}
|
||||
|
||||
public synchronized int getIndexOfSession(TerminalSession terminalSession) {
|
||||
if (terminalSession == null) return -1;
|
||||
|
||||
for (int i = 0; i < mShellManager.mTermuxSessions.size(); i++) {
|
||||
if (mShellManager.mTermuxSessions.get(i).getTerminalSession().equals(terminalSession))
|
||||
for (int i = 0; i < mTermuxSessions.size(); i++) {
|
||||
if (mTermuxSessions.get(i).getTerminalSession().equals(terminalSession))
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
|
|
@ -918,40 +828,20 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
|
|||
|
||||
public synchronized TerminalSession getTerminalSessionForHandle(String sessionHandle) {
|
||||
TerminalSession terminalSession;
|
||||
for (int i = 0, len = mShellManager.mTermuxSessions.size(); i < len; i++) {
|
||||
terminalSession = mShellManager.mTermuxSessions.get(i).getTerminalSession();
|
||||
for (int i = 0, len = mTermuxSessions.size(); i < len; i++) {
|
||||
terminalSession = mTermuxSessions.get(i).getTerminalSession();
|
||||
if (terminalSession.mHandle.equals(sessionHandle))
|
||||
return terminalSession;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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 static synchronized int getNextExecutionId() {
|
||||
return EXECUTION_ID++;
|
||||
}
|
||||
|
||||
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,5 +1,6 @@
|
|||
package com.termux.app.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
|
@ -11,12 +12,10 @@ 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 AppCompatActivity {
|
||||
public final class HelpActivity extends Activity {
|
||||
|
||||
WebView mWebView;
|
||||
|
||||
|
|
@ -35,6 +34,7 @@ public final class HelpActivity extends AppCompatActivity {
|
|||
mWebView = new WebView(this);
|
||||
WebSettings settings = mWebView.getSettings();
|
||||
settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
|
||||
settings.setAppCacheEnabled(false);
|
||||
setContentView(progressLayout);
|
||||
mWebView.clearCache(true);
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ 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;
|
||||
|
|
@ -15,25 +16,20 @@ 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.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.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.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()
|
||||
|
|
@ -41,9 +37,11 @@ public class SettingsActivity extends AppCompatActivity {
|
|||
.replace(R.id.settings, new RootPreferencesFragment())
|
||||
.commit();
|
||||
}
|
||||
|
||||
AppCompatActivityUtils.setToolbar(this, com.termux.shared.R.id.toolbar);
|
||||
AppCompatActivityUtils.setShowBackButtonInActionBar(this, true);
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -60,17 +58,12 @@ public class SettingsActivity extends AppCompatActivity {
|
|||
|
||||
setPreferencesFromResource(R.xml.root_preferences, rootKey);
|
||||
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
configureTermuxAPIPreference(context);
|
||||
configureTermuxFloatPreference(context);
|
||||
configureTermuxTaskerPreference(context);
|
||||
configureTermuxWidgetPreference(context);
|
||||
configureAboutPreference(context);
|
||||
configureDonatePreference(context);
|
||||
}
|
||||
}.start();
|
||||
configureTermuxAPIPreference(context);
|
||||
configureTermuxFloatPreference(context);
|
||||
configureTermuxTaskerPreference(context);
|
||||
configureTermuxWidgetPreference(context);
|
||||
configureAboutPreference(context);
|
||||
configureDonatePreference(context);
|
||||
}
|
||||
|
||||
private void configureTermuxAPIPreference(@NonNull Context context) {
|
||||
|
|
@ -119,20 +112,22 @@ public class SettingsActivity extends AppCompatActivity {
|
|||
String title = "About";
|
||||
|
||||
StringBuilder aboutString = new StringBuilder();
|
||||
aboutString.append(TermuxUtils.getAppInfoMarkdownString(context, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGES));
|
||||
aboutString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context, true));
|
||||
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("\n\n").append(TermuxUtils.getImportantLinksMarkdownString(context));
|
||||
|
||||
String userActionName = UserAction.ABOUT.getName();
|
||||
|
||||
ReportInfo reportInfo = new ReportInfo(userActionName,
|
||||
TermuxConstants.TERMUX_APP.TERMUX_SETTINGS_ACTIVITY_NAME, title);
|
||||
reportInfo.setReportString(aboutString.toString());
|
||||
reportInfo.setReportSaveFileLabelAndPath(userActionName,
|
||||
ReportActivity.startReportActivity(context, new ReportInfo(userActionName,
|
||||
TermuxConstants.TERMUX_APP.TERMUX_SETTINGS_ACTIVITY_NAME, title, null,
|
||||
aboutString.toString(), null, false,
|
||||
userActionName,
|
||||
Environment.getExternalStorageDirectory() + "/" +
|
||||
FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true));
|
||||
|
||||
ReportActivity.startReportActivity(context, reportInfo);
|
||||
FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true)));
|
||||
}
|
||||
}.start();
|
||||
|
||||
|
|
@ -159,7 +154,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,91 +0,0 @@
|
|||
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.termux.settings.preferences.TermuxAPIAppSharedPreferences;
|
||||
import com.termux.shared.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.termux.settings.preferences.TermuxFloatAppSharedPreferences;
|
||||
import com.termux.shared.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.termux.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.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.termux.settings.preferences.TermuxTaskerAppSharedPreferences;
|
||||
import com.termux.shared.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.termux.settings.preferences.TermuxWidgetAppSharedPreferences;
|
||||
import com.termux.shared.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.termux.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.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(false);
|
||||
return mPreferences.arePluginErrorNotificationsEnabled();
|
||||
case "crash_report_notifications_enabled":
|
||||
return mPreferences.areCrashReportNotificationsEnabled(false);
|
||||
return mPreferences.areCrashReportNotificationsEnabled();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import androidx.preference.PreferenceFragmentCompat;
|
|||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.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.termux.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.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.termux.settings.preferences.TermuxAPIAppSharedPreferences;
|
||||
import com.termux.shared.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.termux.settings.preferences.TermuxFloatAppSharedPreferences;
|
||||
import com.termux.shared.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.termux.settings.preferences.TermuxTaskerAppSharedPreferences;
|
||||
import com.termux.shared.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.termux.settings.preferences.TermuxWidgetAppSharedPreferences;
|
||||
import com.termux.shared.settings.preferences.TermuxWidgetAppSharedPreferences;
|
||||
|
||||
@Keep
|
||||
public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ 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;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
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,9 +20,7 @@ import androidx.core.content.ContextCompat;
|
|||
|
||||
import com.termux.R;
|
||||
import com.termux.app.TermuxActivity;
|
||||
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.shared.shell.TermuxSession;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
import java.util.List;
|
||||
|
|
@ -57,9 +55,9 @@ public class TermuxSessionsListViewController extends ArrayAdapter<TermuxSession
|
|||
return sessionRowView;
|
||||
}
|
||||
|
||||
boolean shouldEnableDarkTheme = ThemeUtils.shouldEnableDarkTheme(mActivity, NightMode.getAppNightMode().getName());
|
||||
boolean isUsingBlackUI = mActivity.getProperties().isUsingBlackUI();
|
||||
|
||||
if (shouldEnableDarkTheme) {
|
||||
if (isUsingBlackUI) {
|
||||
sessionTitleView.setBackground(
|
||||
ContextCompat.getDrawable(mActivity, R.drawable.session_background_black_selected)
|
||||
);
|
||||
|
|
@ -86,7 +84,7 @@ public class TermuxSessionsListViewController extends ArrayAdapter<TermuxSession
|
|||
} else {
|
||||
sessionTitleView.setPaintFlags(sessionTitleView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
|
||||
}
|
||||
int defaultColor = shouldEnableDarkTheme ? Color.WHITE : Color.BLACK;
|
||||
int defaultColor = isUsingBlackUI ? Color.WHITE : Color.BLACK;
|
||||
int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? defaultColor : Color.RED;
|
||||
sessionTitleView.setTextColor(color);
|
||||
return sessionRowView;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
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;
|
||||
|
|
@ -13,23 +12,19 @@ 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.termux.terminal.TermuxTerminalSessionClientBase;
|
||||
import com.termux.shared.terminal.TermuxTerminalSessionClientBase;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.app.TermuxService;
|
||||
import com.termux.shared.termux.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.shared.termux.terminal.io.BellHandler;
|
||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.shared.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;
|
||||
|
|
@ -37,8 +32,7 @@ import java.io.FileInputStream;
|
|||
import java.io.InputStream;
|
||||
import java.util.Properties;
|
||||
|
||||
/** The {@link TerminalSessionClient} implementation that may require an {@link Activity} for its interface methods. */
|
||||
public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionClientBase {
|
||||
public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase {
|
||||
|
||||
private final TermuxActivity mActivity;
|
||||
|
||||
|
|
@ -48,9 +42,9 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
|
|||
|
||||
private int mBellSoundId;
|
||||
|
||||
private static final String LOG_TAG = "TermuxTerminalSessionActivityClient";
|
||||
private static final String LOG_TAG = "TermuxTerminalSessionClient";
|
||||
|
||||
public TermuxTerminalSessionActivityClient(TermuxActivity activity) {
|
||||
public TermuxTerminalSessionClient(TermuxActivity activity) {
|
||||
this.mActivity = activity;
|
||||
}
|
||||
|
||||
|
|
@ -86,7 +80,7 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
|
|||
// 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
|
||||
loadBellSoundPool();
|
||||
getBellSoundPool();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -107,7 +101,7 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
|
|||
/**
|
||||
* Should be called when mActivity.reloadActivityStyling() is called
|
||||
*/
|
||||
public void onReloadActivityStyling() {
|
||||
public void onReload() {
|
||||
// Set terminal fonts and colors
|
||||
checkForFontAndColors();
|
||||
}
|
||||
|
|
@ -115,14 +109,14 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
|
|||
|
||||
|
||||
@Override
|
||||
public void onTextChanged(@NonNull TerminalSession changedSession) {
|
||||
public void onTextChanged(TerminalSession changedSession) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
if (mActivity.getCurrentSession() == changedSession) mActivity.getTerminalView().onScreenUpdated();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTitleChanged(@NonNull TerminalSession updatedSession) {
|
||||
public void onTitleChanged(TerminalSession updatedSession) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
if (updatedSession != mActivity.getCurrentSession()) {
|
||||
|
|
@ -136,7 +130,7 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onSessionFinished(@NonNull TerminalSession finishedSession) {
|
||||
public void onSessionFinished(final TerminalSession finishedSession) {
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
|
||||
if (service == null || service.wantsToStop()) {
|
||||
|
|
@ -181,14 +175,14 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onCopyTextToClipboard(@NonNull TerminalSession session, String text) {
|
||||
public void onCopyTextToClipboard(TerminalSession session, String text) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
ShareUtils.copyTextToClipboard(mActivity, text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPasteTextFromClipboard(@Nullable TerminalSession session) {
|
||||
public void onPasteTextFromClipboard(TerminalSession session) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
String text = ShareUtils.getTextStringFromClipboardIfSet(mActivity, true);
|
||||
|
|
@ -197,7 +191,7 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onBell(@NonNull TerminalSession session) {
|
||||
public void onBell(TerminalSession session) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
switch (mActivity.getProperties().getBellBehaviour()) {
|
||||
|
|
@ -205,9 +199,7 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
|
|||
BellHandler.getInstance(mActivity).doBell();
|
||||
break;
|
||||
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_BEEP:
|
||||
loadBellSoundPool();
|
||||
if (mBellSoundPool != null)
|
||||
mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
|
||||
getBellSoundPool().play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
|
||||
break;
|
||||
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_IGNORE:
|
||||
// Ignore the bell character.
|
||||
|
|
@ -216,7 +208,7 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onColorsChanged(@NonNull TerminalSession changedSession) {
|
||||
public void onColorsChanged(TerminalSession changedSession) {
|
||||
if (mActivity.getCurrentSession() == changedSession)
|
||||
updateBackgroundColor();
|
||||
}
|
||||
|
|
@ -234,17 +226,6 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
|
|||
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
|
||||
*/
|
||||
|
|
@ -263,20 +244,17 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
|
|||
|
||||
|
||||
|
||||
/** Load mBellSoundPool */
|
||||
private synchronized void loadBellSoundPool() {
|
||||
/** Initialize and get mBellSoundPool */
|
||||
private synchronized SoundPool getBellSoundPool() {
|
||||
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();
|
||||
|
||||
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);
|
||||
}
|
||||
mBellSoundId = mBellSoundPool.load(mActivity, R.raw.bell, 1);
|
||||
}
|
||||
|
||||
return mBellSoundPool;
|
||||
}
|
||||
|
||||
/** Release mBellSoundPool resources */
|
||||
|
|
@ -345,22 +323,11 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
|
|||
if (sessionToRename == null) return;
|
||||
|
||||
TextInputDialogUtils.textInput(mActivity, R.string.title_rename_session, sessionToRename.mSessionName, R.string.action_rename_session_confirm, text -> {
|
||||
renameSession(sessionToRename, text);
|
||||
sessionToRename.mSessionName = 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;
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
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,10 +2,12 @@ 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.media.AudioManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Gravity;
|
||||
|
|
@ -19,37 +21,35 @@ 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.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.terminal.TermuxTerminalViewClientBase;
|
||||
import com.termux.shared.terminal.io.extrakeys.SpecialButton;
|
||||
import com.termux.shared.termux.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.termux.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
import com.termux.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;
|
||||
|
||||
|
|
@ -57,7 +57,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
|
||||
final TermuxActivity mActivity;
|
||||
|
||||
final TermuxTerminalSessionActivityClient mTermuxTerminalSessionActivityClient;
|
||||
final TermuxTerminalSessionClient mTermuxTerminalSessionClient;
|
||||
|
||||
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
|
||||
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
|
||||
|
|
@ -69,13 +69,11 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
|
||||
private boolean mTerminalCursorBlinkerStateAlreadySet;
|
||||
|
||||
private List<KeyboardShortcut> mSessionShortcuts;
|
||||
|
||||
private static final String LOG_TAG = "TermuxTerminalViewClient";
|
||||
|
||||
public TermuxTerminalViewClient(TermuxActivity activity, TermuxTerminalSessionActivityClient termuxTerminalSessionActivityClient) {
|
||||
public TermuxTerminalViewClient(TermuxActivity activity, TermuxTerminalSessionClient termuxTerminalSessionClient) {
|
||||
this.mActivity = activity;
|
||||
this.mTermuxTerminalSessionActivityClient = termuxTerminalSessionActivityClient;
|
||||
this.mTermuxTerminalSessionClient = termuxTerminalSessionClient;
|
||||
}
|
||||
|
||||
public TermuxActivity getActivity() {
|
||||
|
|
@ -86,8 +84,6 @@ 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());
|
||||
}
|
||||
|
|
@ -111,7 +107,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
*/
|
||||
public void onResume() {
|
||||
// Show the soft keyboard if required
|
||||
setSoftKeyboardState(true, mActivity.isActivityRecreated());
|
||||
setSoftKeyboardState(true, false);
|
||||
|
||||
mTerminalCursorBlinkerStateAlreadySet = false;
|
||||
|
||||
|
|
@ -133,17 +129,10 @@ 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 onReloadActivityStyling() {
|
||||
public void onReload() {
|
||||
// Show the soft keyboard if required
|
||||
setSoftKeyboardState(false, true);
|
||||
|
||||
|
|
@ -189,11 +178,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 = TermuxUrlUtils.extractUrls(wordAtTap);
|
||||
LinkedHashSet<CharSequence> urlSet = UrlUtils.extractUrls(wordAtTap);
|
||||
|
||||
if (!urlSet.isEmpty()) {
|
||||
String url = (String) urlSet.iterator().next();
|
||||
ShareUtils.openUrl(mActivity, url);
|
||||
ShareUtils.openURL(mActivity, url);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -242,7 +231,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
if (handleVirtualKeys(keyCode, e, true)) return true;
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) {
|
||||
mTermuxTerminalSessionActivityClient.removeFinishedSession(currentSession);
|
||||
mTermuxTerminalSessionClient.removeFinishedSession(currentSession);
|
||||
return true;
|
||||
} else if (!mActivity.getProperties().areHardwareKeyboardShortcutsDisabled() &&
|
||||
e.isCtrlPressed() && e.isAltPressed()) {
|
||||
|
|
@ -250,9 +239,9 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
int unicodeChar = e.getUnicodeChar(0);
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || unicodeChar == 'n'/* next */) {
|
||||
mTermuxTerminalSessionActivityClient.switchToSession(true);
|
||||
mTermuxTerminalSessionClient.switchToSession(true);
|
||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_UP || unicodeChar == 'p' /* previous */) {
|
||||
mTermuxTerminalSessionActivityClient.switchToSession(false);
|
||||
mTermuxTerminalSessionClient.switchToSession(false);
|
||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
|
||||
mActivity.getDrawer().openDrawer(Gravity.LEFT);
|
||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
|
||||
|
|
@ -262,9 +251,9 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
} else if (unicodeChar == 'm'/* menu */) {
|
||||
mActivity.getTerminalView().showContextMenu();
|
||||
} else if (unicodeChar == 'r'/* rename */) {
|
||||
mTermuxTerminalSessionActivityClient.renameSession(currentSession);
|
||||
mTermuxTerminalSessionClient.renameSession(currentSession);
|
||||
} else if (unicodeChar == 'c'/* create */) {
|
||||
mTermuxTerminalSessionActivityClient.addNewSession(false, null);
|
||||
mTermuxTerminalSessionClient.addNewSession(false, null);
|
||||
} else if (unicodeChar == 'u' /* urls */) {
|
||||
showUrlSelection();
|
||||
} else if (unicodeChar == 'v') {
|
||||
|
|
@ -277,7 +266,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
changeFontSize(false);
|
||||
} else if (unicodeChar >= '1' && unicodeChar <= '9') {
|
||||
int index = unicodeChar - '1';
|
||||
mTermuxTerminalSessionActivityClient.switchToSession(index);
|
||||
mTermuxTerminalSessionClient.switchToSession(index);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -461,11 +450,11 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
return true;
|
||||
} else if (ctrlDown) {
|
||||
if (codePoint == 106 /* Ctrl+j or \n */ && !session.isRunning()) {
|
||||
mTermuxTerminalSessionActivityClient.removeFinishedSession(session);
|
||||
mTermuxTerminalSessionClient.removeFinishedSession(session);
|
||||
return true;
|
||||
}
|
||||
|
||||
List<KeyboardShortcut> shortcuts = mSessionShortcuts;
|
||||
List<KeyboardShortcut> shortcuts = mActivity.getProperties().getSessionShortcuts();
|
||||
if (shortcuts != null && !shortcuts.isEmpty()) {
|
||||
int codePointLowerCase = Character.toLowerCase(codePoint);
|
||||
for (int i = shortcuts.size() - 1; i >= 0; i--) {
|
||||
|
|
@ -473,16 +462,16 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
if (codePointLowerCase == shortcut.codePoint) {
|
||||
switch (shortcut.shortcutAction) {
|
||||
case TermuxPropertyConstants.ACTION_SHORTCUT_CREATE_SESSION:
|
||||
mTermuxTerminalSessionActivityClient.addNewSession(false, null);
|
||||
mTermuxTerminalSessionClient.addNewSession(false, null);
|
||||
return true;
|
||||
case TermuxPropertyConstants.ACTION_SHORTCUT_NEXT_SESSION:
|
||||
mTermuxTerminalSessionActivityClient.switchToSession(true);
|
||||
mTermuxTerminalSessionClient.switchToSession(true);
|
||||
return true;
|
||||
case TermuxPropertyConstants.ACTION_SHORTCUT_PREVIOUS_SESSION:
|
||||
mTermuxTerminalSessionActivityClient.switchToSession(false);
|
||||
mTermuxTerminalSessionClient.switchToSession(false);
|
||||
return true;
|
||||
case TermuxPropertyConstants.ACTION_SHORTCUT_RENAME_SESSION:
|
||||
mTermuxTerminalSessionActivityClient.renameSession(mActivity.getCurrentSession());
|
||||
mTermuxTerminalSessionClient.renameSession(mActivity.getCurrentSession());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -493,27 +482,6 @@ 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) {
|
||||
|
|
@ -697,7 +665,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
|
||||
String text = ShellUtils.getTerminalSessionTranscriptText(session, true, true);
|
||||
|
||||
LinkedHashSet<CharSequence> urlSet = TermuxUrlUtils.extractUrls(text);
|
||||
LinkedHashSet<CharSequence> urlSet = UrlUtils.extractUrls(text);
|
||||
if (urlSet.isEmpty()) {
|
||||
new AlertDialog.Builder(mActivity).setMessage(R.string.title_select_url_none_found).show();
|
||||
return;
|
||||
|
|
@ -718,7 +686,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;
|
||||
});
|
||||
});
|
||||
|
|
@ -735,8 +703,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(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),
|
||||
mActivity.getString(R.string.action_yes), (dialog, which) -> reportIssueFromTranscript(transcriptText, true),
|
||||
mActivity.getString(R.string.action_no), (dialog, which) -> reportIssueFromTranscript(transcriptText, false),
|
||||
null);
|
||||
}
|
||||
|
||||
|
|
@ -754,19 +722,12 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
reportString.append("\n").append(MarkdownUtils.getMarkdownCodeForString(transcriptText, true));
|
||||
reportString.append("\n##\n");
|
||||
|
||||
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));
|
||||
}
|
||||
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, true));
|
||||
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(mActivity));
|
||||
|
||||
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(mActivity, true));
|
||||
|
||||
if (TermuxBootstrap.isAppPackageManagerAPT()) {
|
||||
String termuxAptInfo = TermuxUtils.geAPTInfoMarkdownString(mActivity);
|
||||
if (termuxAptInfo != null)
|
||||
reportString.append("\n\n").append(termuxAptInfo);
|
||||
}
|
||||
String termuxAptInfo = TermuxUtils.geAPTInfoMarkdownString(mActivity);
|
||||
if (termuxAptInfo != null)
|
||||
reportString.append("\n\n").append(termuxAptInfo);
|
||||
|
||||
if (addTermuxDebugInfo) {
|
||||
String termuxDebugInfo = TermuxUtils.getTermuxDebugMarkdownString(mActivity);
|
||||
|
|
@ -775,16 +736,14 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||
}
|
||||
|
||||
String userActionName = UserAction.REPORT_ISSUE_FROM_TRANSCRIPT.getName();
|
||||
|
||||
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);
|
||||
ReportActivity.startReportActivity(mActivity,
|
||||
new ReportInfo(userActionName,
|
||||
TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY_NAME, title, null,
|
||||
reportString.toString(), "\n\n" + TermuxUtils.getReportIssueMarkdownString(mActivity),
|
||||
false,
|
||||
userActionName,
|
||||
Environment.getExternalStorageDirectory() + "/" +
|
||||
FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true)));
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import androidx.viewpager.widget.ViewPager;
|
|||
|
||||
import com.termux.R;
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.shared.termux.extrakeys.ExtraKeysView;
|
||||
import com.termux.shared.terminal.io.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(mActivity.getTermuxTerminalExtraKeys());
|
||||
extraKeysView.setExtraKeysViewClient(new TermuxTerminalExtraKeys(mActivity.getTerminalView(),
|
||||
mActivity.getTermuxTerminalViewClient(), mActivity.getTermuxTerminalSessionClient()));
|
||||
extraKeysView.setButtonTextAllCaps(mActivity.getProperties().shouldExtraKeysTextBeAllCaps());
|
||||
mActivity.setExtraKeysView(extraKeysView);
|
||||
extraKeysView.reload(mActivity.getTermuxTerminalExtraKeys().getExtraKeysInfo(),
|
||||
mActivity.getTerminalToolbarDefaultHeight());
|
||||
extraKeysView.reload(mActivity.getProperties().getExtraKeysInfo());
|
||||
|
||||
// apply extra keys fix if enabled in prefs
|
||||
if (mActivity.getProperties().isUsingFullScreen() && mActivity.getProperties().isUsingFullScreenWorkAround()) {
|
||||
|
|
|
|||
|
|
@ -7,78 +7,23 @@ import android.view.View;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.app.terminal.TermuxTerminalSessionActivityClient;
|
||||
import com.termux.app.terminal.TermuxTerminalSessionClient;
|
||||
import com.termux.app.terminal.TermuxTerminalViewClient;
|
||||
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.shared.terminal.io.TerminalExtraKeys;
|
||||
import com.termux.view.TerminalView;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
public class TermuxTerminalExtraKeys extends TerminalExtraKeys {
|
||||
|
||||
private ExtraKeysInfo mExtraKeysInfo;
|
||||
|
||||
final TermuxActivity mActivity;
|
||||
final TermuxTerminalViewClient mTermuxTerminalViewClient;
|
||||
final TermuxTerminalSessionActivityClient mTermuxTerminalSessionActivityClient;
|
||||
TermuxTerminalViewClient mTermuxTerminalViewClient;
|
||||
TermuxTerminalSessionClient mTermuxTerminalSessionClient;
|
||||
|
||||
private static final String LOG_TAG = "TermuxTerminalExtraKeys";
|
||||
|
||||
public TermuxTerminalExtraKeys(TermuxActivity activity, @NonNull TerminalView terminalView,
|
||||
public TermuxTerminalExtraKeys(@NonNull TerminalView terminalView,
|
||||
TermuxTerminalViewClient termuxTerminalViewClient,
|
||||
TermuxTerminalSessionActivityClient termuxTerminalSessionActivityClient) {
|
||||
TermuxTerminalSessionClient termuxTerminalSessionClient) {
|
||||
super(terminalView);
|
||||
|
||||
mActivity = activity;
|
||||
mTermuxTerminalViewClient = termuxTerminalViewClient;
|
||||
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;
|
||||
mTermuxTerminalSessionClient = termuxTerminalSessionClient;
|
||||
}
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
|
|
@ -94,12 +39,8 @@ public class TermuxTerminalExtraKeys extends TerminalExtraKeys {
|
|||
else
|
||||
drawerLayout.openDrawer(Gravity.LEFT);
|
||||
} else if ("PASTE".equals(key)) {
|
||||
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();
|
||||
if(mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.onPasteTextFromClipboard(null);
|
||||
} else {
|
||||
super.onTerminalExtraKeyButtonClick(view, key, ctrlDown, altDown, shiftDown, fnDown);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,214 @@
|
|||
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,45 +1,43 @@
|
|||
package com.termux.shared.termux.plugins;
|
||||
package com.termux.app.utils;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.shared.R;
|
||||
import com.termux.R;
|
||||
import com.termux.shared.activities.ReportActivity;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.termux.file.TermuxFileUtils;
|
||||
import com.termux.shared.shell.command.result.ResultConfig;
|
||||
import com.termux.shared.shell.command.result.ResultData;
|
||||
import com.termux.shared.errors.Errno;
|
||||
import com.termux.shared.errors.Error;
|
||||
import com.termux.shared.file.TermuxFileUtils;
|
||||
import com.termux.shared.models.ResultConfig;
|
||||
import com.termux.shared.models.ResultData;
|
||||
import com.termux.shared.models.errors.Errno;
|
||||
import com.termux.shared.models.errors.Error;
|
||||
import com.termux.shared.notification.NotificationUtils;
|
||||
import com.termux.shared.termux.models.UserAction;
|
||||
import com.termux.shared.termux.notification.TermuxNotificationUtils;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxPreferenceConstants;
|
||||
import com.termux.shared.shell.command.result.ResultSender;
|
||||
import com.termux.shared.notification.TermuxNotificationUtils;
|
||||
import com.termux.shared.shell.ResultSender;
|
||||
import com.termux.shared.shell.ShellUtils;
|
||||
import com.termux.shared.android.AndroidUtils;
|
||||
import com.termux.shared.termux.AndroidUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxPreferenceConstants.TERMUX_APP;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_APP;
|
||||
import com.termux.shared.settings.properties.SharedProperties;
|
||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.shared.models.ReportInfo;
|
||||
import com.termux.shared.termux.settings.properties.TermuxAppSharedProperties;
|
||||
import com.termux.shared.shell.command.ExecutionCommand;
|
||||
import com.termux.shared.models.ExecutionCommand;
|
||||
import com.termux.app.models.UserAction;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
|
||||
public class TermuxPluginUtils {
|
||||
public class PluginUtils {
|
||||
|
||||
private static final String LOG_TAG = "TermuxPluginUtils";
|
||||
private static final String LOG_TAG = "PluginUtils";
|
||||
|
||||
/**
|
||||
* Process {@link ExecutionCommand} result.
|
||||
|
|
@ -91,11 +89,8 @@ public class TermuxPluginUtils {
|
|||
Logger.logDebugExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, true, isExecutionCommandLoggingEnabled));
|
||||
|
||||
// Flash and send notification for the error
|
||||
sendPluginCommandErrorNotification(context, logTag, null,
|
||||
ResultData.getErrorsListMinimalString(resultData),
|
||||
ExecutionCommand.getExecutionCommandMarkdownString(executionCommand),
|
||||
false, true, TermuxUtils.AppInfoMode.TERMUX_AND_CALLING_PACKAGE,true,
|
||||
executionCommand.resultConfig.resultPendingIntent != null ? executionCommand.resultConfig.resultPendingIntent.getCreatorPackage(): null);
|
||||
Logger.showToast(context, ResultData.getErrorsListMinimalString(resultData), true);
|
||||
sendPluginCommandErrorNotification(context, logTag, executionCommand, ResultData.getErrorsListMinimalString(resultData));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -104,28 +99,6 @@ public class TermuxPluginUtils {
|
|||
executionCommand.setState(ExecutionCommand.ExecutionState.SUCCESS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set {@link ExecutionCommand} state to {@link Errno#ERRNO_FAILED} with {@code errmsg} and
|
||||
* process error with {@link #processPluginExecutionCommandError(Context, String, ExecutionCommand, boolean)}.
|
||||
*
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param logTag The log tag to use for logging.
|
||||
* @param executionCommand The {@link ExecutionCommand} that failed.
|
||||
* @param forceNotification If set to {@code true}, then a flash and notification will be shown
|
||||
* regardless of if pending intent is {@code null} or
|
||||
* {@link TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED}
|
||||
* is {@code false}.
|
||||
* @param errmsg The error message to set.
|
||||
*/
|
||||
public static void setAndProcessPluginExecutionCommandError(final Context context, String logTag,
|
||||
final ExecutionCommand executionCommand,
|
||||
boolean forceNotification,
|
||||
@NonNull String errmsg) {
|
||||
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||
processPluginExecutionCommandError(context, logTag, executionCommand, forceNotification);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process {@link ExecutionCommand} error.
|
||||
*
|
||||
|
|
@ -151,9 +124,7 @@ public class TermuxPluginUtils {
|
|||
* {@link TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED}
|
||||
* is {@code false}.
|
||||
*/
|
||||
public static void processPluginExecutionCommandError(final Context context, String logTag,
|
||||
final ExecutionCommand executionCommand,
|
||||
boolean forceNotification) {
|
||||
public static void processPluginExecutionCommandError(final Context context, String logTag, final ExecutionCommand executionCommand, boolean forceNotification) {
|
||||
if (context == null || executionCommand == null) return;
|
||||
|
||||
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||
|
|
@ -169,9 +140,7 @@ public class TermuxPluginUtils {
|
|||
boolean isExecutionCommandLoggingEnabled = Logger.shouldEnableLoggingForCustomLogLevel(executionCommand.backgroundCustomLogLevel);
|
||||
|
||||
// Log the error and any exception. ResultData should not be logged if pending result since ResultSender will do it
|
||||
Logger.logError(logTag, "Processing plugin execution error for:\n" + executionCommand.getCommandIdAndLabelLogString());
|
||||
Logger.logError(logTag, "Set log level to debug or higher to see error in logs");
|
||||
Logger.logErrorPrivateExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true,
|
||||
Logger.logErrorExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true,
|
||||
!isPluginExecutionCommandWithPendingResult, isExecutionCommandLoggingEnabled));
|
||||
|
||||
// If execution command was started by a plugin which expects the result back
|
||||
|
|
@ -188,7 +157,7 @@ public class TermuxPluginUtils {
|
|||
if (error != null) {
|
||||
// error will be added to existing Errors
|
||||
resultData.setStateFailed(error);
|
||||
Logger.logErrorPrivateExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, true, isExecutionCommandLoggingEnabled));
|
||||
Logger.logErrorExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, true, isExecutionCommandLoggingEnabled));
|
||||
forceNotification = true;
|
||||
}
|
||||
|
||||
|
|
@ -196,12 +165,17 @@ public class TermuxPluginUtils {
|
|||
if (!forceNotification) return;
|
||||
}
|
||||
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
||||
if (preferences == null) return;
|
||||
|
||||
// If user has disabled notifications for plugin commands, then just return
|
||||
if (!preferences.arePluginErrorNotificationsEnabled() && !forceNotification)
|
||||
return;
|
||||
|
||||
// Flash and send notification for the error
|
||||
sendPluginCommandErrorNotification(context, logTag, null,
|
||||
ResultData.getErrorsListMinimalString(resultData),
|
||||
ExecutionCommand.getExecutionCommandMarkdownString(executionCommand),
|
||||
forceNotification, true, TermuxUtils.AppInfoMode.TERMUX_AND_CALLING_PACKAGE, true,
|
||||
executionCommand.resultConfig.resultPendingIntent != null ? executionCommand.resultConfig.resultPendingIntent.getCreatorPackage(): null);
|
||||
Logger.showToast(context, ResultData.getErrorsListMinimalString(resultData), true);
|
||||
sendPluginCommandErrorNotification(context, logTag, executionCommand, ResultData.getErrorsListMinimalString(resultData));
|
||||
|
||||
}
|
||||
|
||||
/** Set variables which will be used by {@link ResultSender#sendCommandResultData(Context, String, String, ResultConfig, ResultData, boolean)}
|
||||
|
|
@ -234,173 +208,57 @@ public class TermuxPluginUtils {
|
|||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Send a plugin error report notification for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
|
||||
* Send an error notification for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
|
||||
* and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
||||
*
|
||||
* @param currentPackageContext The {@link Context} of current package.
|
||||
* @param logTag The log tag to use for logging.
|
||||
* @param title The title for the error report and notification.
|
||||
* @param message The message for the error report.
|
||||
* @param throwable The {@link Throwable} for the error report.
|
||||
*/
|
||||
public static void sendPluginCommandErrorNotification(final Context currentPackageContext, String logTag,
|
||||
CharSequence title, String message, Throwable throwable) {
|
||||
sendPluginCommandErrorNotification(currentPackageContext, logTag,
|
||||
title, message,
|
||||
MarkdownUtils.getMarkdownCodeForString(Logger.getMessageAndStackTraceString(message, throwable), true),
|
||||
false, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a plugin error report notification for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
|
||||
* and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
||||
*
|
||||
* @param currentPackageContext The {@link Context} of current package.
|
||||
* @param logTag The log tag to use for logging.
|
||||
* @param title The title for the error report and notification.
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param executionCommand The {@link ExecutionCommand} that failed.
|
||||
* @param notificationTextString The text of the notification.
|
||||
* @param message The message for the error report.
|
||||
*/
|
||||
public static void sendPluginCommandErrorNotification(final Context currentPackageContext, String logTag,
|
||||
CharSequence title, String notificationTextString,
|
||||
String message) {
|
||||
sendPluginCommandErrorNotification(currentPackageContext, logTag,
|
||||
title, notificationTextString, message,
|
||||
false, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a plugin error report notification for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
|
||||
* and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
||||
*
|
||||
* @param currentPackageContext The {@link Context} of current package.
|
||||
* @param logTag The log tag to use for logging.
|
||||
* @param title The title for the error report and notification.
|
||||
* @param notificationTextString The text of the notification.
|
||||
* @param message The message for the error 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_PLUGIN_ERROR_NOTIFICATIONS_ENABLED}
|
||||
* is {@code false}.
|
||||
* @param showToast If set to {@code true}, then a toast will be shown for {@code notificationTextString}.
|
||||
* @param addDeviceInfo If set to {@code true}, then device info should be appended to the message.
|
||||
*/
|
||||
public static void sendPluginCommandErrorNotification(final Context currentPackageContext, String logTag,
|
||||
CharSequence title, String notificationTextString,
|
||||
String message, boolean forceNotification,
|
||||
boolean showToast,
|
||||
boolean addDeviceInfo) {
|
||||
sendPluginCommandErrorNotification(currentPackageContext, logTag,
|
||||
title, notificationTextString, "## " + title + "\n\n" + message + "\n\n",
|
||||
forceNotification, showToast, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGE, addDeviceInfo, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a plugin error notification for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
|
||||
* and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
||||
*
|
||||
* @param currentPackageContext The {@link Context} of current package.
|
||||
* @param logTag The log tag to use for logging.
|
||||
* @param title The title for the error report and notification.
|
||||
* @param notificationTextString The text of the notification.
|
||||
* @param message The message for the error 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_PLUGIN_ERROR_NOTIFICATIONS_ENABLED}
|
||||
* is {@code false}.
|
||||
* @param showToast If set to {@code true}, then a toast will be shown for {@code notificationTextString}.
|
||||
* @param appInfoMode The {@link TermuxUtils.AppInfoMode} to use to add app info to the message.
|
||||
* Set to {@code null} if app info should not be appended to the message.
|
||||
* @param addDeviceInfo If set to {@code true}, then device info should be appended to the message.
|
||||
* @param callingPackageName The optional package name of the app for which the plugin command
|
||||
* was run.
|
||||
*/
|
||||
public static void sendPluginCommandErrorNotification(Context currentPackageContext, String logTag,
|
||||
CharSequence title,
|
||||
String notificationTextString,
|
||||
String message, boolean forceNotification,
|
||||
boolean showToast,
|
||||
TermuxUtils.AppInfoMode appInfoMode,
|
||||
boolean addDeviceInfo,
|
||||
String callingPackageName) {
|
||||
// Note: Do not change currentPackageContext or termuxPackageContext passed to functions or things will break
|
||||
|
||||
if (currentPackageContext == null) return;
|
||||
String currentPackageName = currentPackageContext.getPackageName();
|
||||
|
||||
final Context termuxPackageContext = TermuxUtils.getTermuxPackageContext(currentPackageContext);
|
||||
if (termuxPackageContext == null) {
|
||||
Logger.logWarn(LOG_TAG, "Ignoring call to sendPluginCommandErrorNotification() since failed to get \"" + TermuxConstants.TERMUX_PACKAGE_NAME + "\" package context from \"" + currentPackageName + "\" context");
|
||||
return;
|
||||
}
|
||||
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(termuxPackageContext);
|
||||
if (preferences == null) return;
|
||||
|
||||
// If user has disabled notifications for plugin commands, then just return
|
||||
if (!preferences.arePluginErrorNotificationsEnabled(true) && !forceNotification)
|
||||
return;
|
||||
|
||||
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||
|
||||
if (showToast)
|
||||
Logger.showToast(currentPackageContext, notificationTextString, true);
|
||||
|
||||
public static void sendPluginCommandErrorNotification(Context context, String logTag, ExecutionCommand executionCommand, String notificationTextString) {
|
||||
// Send a notification to show the error which when clicked will open the ReportActivity
|
||||
// to show the details of the error
|
||||
if (title == null || title.toString().isEmpty())
|
||||
title = TermuxConstants.TERMUX_APP_NAME + " Plugin Execution Command Error";
|
||||
String title = TermuxConstants.TERMUX_APP_NAME + " Plugin Execution Command Error";
|
||||
|
||||
Logger.logDebug(logTag, "Sending \"" + title + "\" notification.");
|
||||
StringBuilder reportString = new StringBuilder();
|
||||
|
||||
StringBuilder reportString = new StringBuilder(message);
|
||||
|
||||
if (appInfoMode != null)
|
||||
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(currentPackageContext, appInfoMode,
|
||||
callingPackageName != null ? callingPackageName : currentPackageName));
|
||||
|
||||
if (addDeviceInfo)
|
||||
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(currentPackageContext, true));
|
||||
reportString.append(ExecutionCommand.getExecutionCommandMarkdownString(executionCommand));
|
||||
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true));
|
||||
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
|
||||
|
||||
String userActionName = UserAction.PLUGIN_EXECUTION_COMMAND.getName();
|
||||
|
||||
ReportInfo reportInfo = new ReportInfo(userActionName, logTag, title.toString());
|
||||
reportInfo.setReportString(reportString.toString());
|
||||
reportInfo.setReportStringSuffix("\n\n" + TermuxUtils.getReportIssueMarkdownString(currentPackageContext));
|
||||
reportInfo.setAddReportInfoHeaderToMarkdown(true);
|
||||
reportInfo.setReportSaveFileLabelAndPath(userActionName,
|
||||
Environment.getExternalStorageDirectory() + "/" +
|
||||
FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true));
|
||||
|
||||
ReportActivity.NewInstanceResult result = ReportActivity.newInstance(termuxPackageContext, reportInfo);
|
||||
ReportActivity.NewInstanceResult result = ReportActivity.newInstance(context,
|
||||
new ReportInfo(userActionName, logTag, title, null,
|
||||
reportString.toString(), null,true,
|
||||
userActionName,
|
||||
Environment.getExternalStorageDirectory() + "/" +
|
||||
FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true)));
|
||||
if (result.contentIntent == null) return;
|
||||
|
||||
// Must ensure result code for PendingIntents and id for notification are unique otherwise will override previous
|
||||
int nextNotificationId = TermuxNotificationUtils.getNextNotificationId(termuxPackageContext);
|
||||
int nextNotificationId = TermuxNotificationUtils.getNextNotificationId(context);
|
||||
|
||||
PendingIntent contentIntent = PendingIntent.getActivity(termuxPackageContext, nextNotificationId, result.contentIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
PendingIntent contentIntent = PendingIntent.getActivity(context, nextNotificationId, result.contentIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
PendingIntent deleteIntent = null;
|
||||
if (result.deleteIntent != null)
|
||||
deleteIntent = PendingIntent.getBroadcast(termuxPackageContext, nextNotificationId, result.deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
deleteIntent = PendingIntent.getBroadcast(context, nextNotificationId, result.deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
// Setup the notification channel if not already set up
|
||||
setupPluginCommandErrorsNotificationChannel(termuxPackageContext);
|
||||
setupPluginCommandErrorsNotificationChannel(context);
|
||||
|
||||
// Use markdown in notification
|
||||
CharSequence notificationTextCharSequence = MarkdownUtils.getSpannedMarkdownText(termuxPackageContext, notificationTextString);
|
||||
CharSequence notificationTextCharSequence = MarkdownUtils.getSpannedMarkdownText(context, notificationTextString);
|
||||
//CharSequence notificationTextCharSequence = notificationTextString;
|
||||
|
||||
// Build the notification
|
||||
Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(currentPackageContext, termuxPackageContext,
|
||||
title, notificationTextCharSequence, notificationTextCharSequence, contentIntent, deleteIntent,
|
||||
NotificationUtils.NOTIFICATION_MODE_VIBRATE);
|
||||
Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(context, title,
|
||||
notificationTextCharSequence, notificationTextCharSequence, contentIntent, deleteIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
|
||||
if (builder == null) return;
|
||||
|
||||
// Send the notification
|
||||
NotificationManager notificationManager = NotificationUtils.getNotificationManager(termuxPackageContext);
|
||||
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
|
||||
if (notificationManager != null)
|
||||
notificationManager.notify(nextNotificationId, builder.build());
|
||||
}
|
||||
|
|
@ -409,8 +267,7 @@ public class TermuxPluginUtils {
|
|||
* Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
|
||||
* and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
||||
*
|
||||
* @param currentPackageContext The {@link Context} of current package.
|
||||
* @param termuxPackageContext The {@link Context} of termux package.
|
||||
* @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.
|
||||
|
|
@ -420,18 +277,29 @@ public class TermuxPluginUtils {
|
|||
* @return Returns the {@link Notification.Builder}.
|
||||
*/
|
||||
@Nullable
|
||||
public static Notification.Builder getPluginCommandErrorsNotificationBuilder(final Context currentPackageContext,
|
||||
final Context termuxPackageContext,
|
||||
final CharSequence title,
|
||||
final CharSequence notificationText,
|
||||
final CharSequence notificationBigText,
|
||||
final PendingIntent contentIntent,
|
||||
final PendingIntent deleteIntent,
|
||||
final int notificationMode) {
|
||||
return TermuxNotificationUtils.getTermuxOrPluginAppNotificationBuilder(
|
||||
currentPackageContext, termuxPackageContext,
|
||||
public static Notification.Builder getPluginCommandErrorsNotificationBuilder(
|
||||
final Context context, final CharSequence title, final CharSequence notificationText,
|
||||
final CharSequence notificationBigText, final PendingIntent contentIntent, final PendingIntent deleteIntent, final int notificationMode) {
|
||||
|
||||
Notification.Builder builder = NotificationUtils.geNotificationBuilder(context,
|
||||
TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -441,7 +309,6 @@ public class TermuxPluginUtils {
|
|||
* @param context The {@link Context} for operations.
|
||||
*/
|
||||
public static void setupPluginCommandErrorsNotificationChannel(final Context context) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
|
||||
NotificationUtils.setupNotificationChannel(context, TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID,
|
||||
TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
|
||||
}
|
||||
|
|
@ -456,9 +323,8 @@ public class TermuxPluginUtils {
|
|||
*/
|
||||
public static String checkIfAllowExternalAppsPolicyIsViolated(final Context context, String apiName) {
|
||||
String errmsg = null;
|
||||
|
||||
TermuxAppSharedProperties mProperties = TermuxAppSharedProperties.getProperties();
|
||||
if (mProperties == null || !mProperties.shouldAllowExternalApps()) {
|
||||
if (!SharedProperties.isPropertyValueTrue(context, TermuxPropertyConstants.getTermuxPropertiesFile(),
|
||||
TermuxConstants.PROP_ALLOW_EXTERNAL_APPS, true)) {
|
||||
errmsg = context.getString(R.string.error_allow_external_apps_ungranted, apiName,
|
||||
TermuxFileUtils.getUnExpandedTermuxPath(TermuxConstants.TERMUX_PROPERTIES_PRIMARY_FILE_PATH));
|
||||
}
|
||||
|
|
@ -1,30 +1,21 @@
|
|||
package com.termux.app.api.file;
|
||||
package com.termux.filepicker;
|
||||
|
||||
import android.content.Context;
|
||||
import android.app.Activity;
|
||||
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.net.uri.UriScheme;
|
||||
import com.termux.shared.termux.interact.TextInputDialogUtils;
|
||||
import com.termux.shared.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;
|
||||
|
|
@ -36,7 +27,7 @@ import java.io.InputStream;
|
|||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class FileReceiverActivity extends AppCompatActivity {
|
||||
public class TermuxFileReceiverActivity extends Activity {
|
||||
|
||||
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";
|
||||
|
|
@ -52,7 +43,7 @@ public class FileReceiverActivity extends AppCompatActivity {
|
|||
|
||||
private static final String API_TAG = TermuxConstants.TERMUX_APP_NAME + "FileReceiver";
|
||||
|
||||
private static final String LOG_TAG = "FileReceiverActivity";
|
||||
private static final String LOG_TAG = "TermuxFileReceiverActivity";
|
||||
|
||||
static boolean isSharedTextAnUrl(String sharedText) {
|
||||
if (sharedText == null || sharedText.isEmpty()) return false;
|
||||
|
|
@ -100,13 +91,11 @@ public class FileReceiverActivity extends AppCompatActivity {
|
|||
return;
|
||||
}
|
||||
|
||||
if (UriScheme.SCHEME_CONTENT.equals(scheme)) {
|
||||
if ("content".equals(scheme)) {
|
||||
handleContentUri(dataUri, sharedTitle);
|
||||
} 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);
|
||||
} else if ("file".equals(scheme)) {
|
||||
// When e.g. clicking on a downloaded apk:
|
||||
String path = dataUri.getPath();
|
||||
if (DataUtils.isNullOrEmpty(path)) {
|
||||
showErrorDialogAndQuit("File path from data uri is null, empty or invalid.");
|
||||
return;
|
||||
|
|
@ -134,10 +123,8 @@ public class FileReceiverActivity extends AppCompatActivity {
|
|||
dialog -> finish());
|
||||
}
|
||||
|
||||
void handleContentUri(@NonNull final Uri uri, String subjectFromIntent) {
|
||||
void handleContentUri(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};
|
||||
|
|
@ -149,7 +136,6 @@ public class FileReceiverActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
if (attachmentFileName == null) attachmentFileName = subjectFromIntent;
|
||||
if (attachmentFileName == null) attachmentFileName = UriUtils.getUriFileBasename(uri, true);
|
||||
|
||||
InputStream in = getContentResolver().openInputStream(uri);
|
||||
promptNameAndSave(in, attachmentFileName);
|
||||
|
|
@ -160,36 +146,35 @@ public class FileReceiverActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
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 = UriUtils.getFileUri(EDITOR_PROGRAM);
|
||||
final Uri scriptUri = new Uri.Builder().scheme("file").path(EDITOR_PROGRAM).build();
|
||||
|
||||
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();
|
||||
},
|
||||
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();
|
||||
},
|
||||
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(FileReceiverActivity.this, TermuxService.class);
|
||||
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
||||
startService(executeIntent);
|
||||
finish();
|
||||
},
|
||||
|
|
@ -240,48 +225,13 @@ public class FileReceiverActivity extends AppCompatActivity {
|
|||
//noinspection ResultOfMethodCallIgnored
|
||||
urlOpenerProgramFile.setExecutable(true);
|
||||
|
||||
final Uri urlOpenerProgramUri = UriUtils.getFileUri(URL_OPENER_PROGRAM);
|
||||
final Uri urlOpenerProgramUri = new Uri.Builder().scheme("file").path(URL_OPENER_PROGRAM).build();
|
||||
|
||||
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, urlOpenerProgramUri);
|
||||
executeIntent.setClass(FileReceiverActivity.this, TermuxService.class);
|
||||
executeIntent.setClass(TermuxFileReceiverActivity.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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,16 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include
|
||||
layout="@layout/partial_primary_toolbar"
|
||||
android:id="@+id/partial_primary_toolbar"/>
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
|
|||
|
|
@ -38,12 +38,12 @@
|
|||
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:background="?attr/termuxActivityDrawerBackground">
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
|
|
@ -55,8 +55,7 @@
|
|||
android:layout_height="40dp"
|
||||
android:src="@drawable/ic_settings"
|
||||
android:background="@null"
|
||||
android:contentDescription="@string/action_open_settings"
|
||||
app:tint="?attr/termuxActivityDrawerImageTint" />
|
||||
android:contentDescription="@string/action_open_settings" />
|
||||
</LinearLayout>
|
||||
|
||||
<ListView
|
||||
|
|
@ -74,7 +73,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
<Button
|
||||
android:id="@+id/toggle_keyboard_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="match_parent"
|
||||
|
|
@ -82,7 +81,7 @@
|
|||
android:layout_weight="1"
|
||||
android:text="@string/action_toggle_soft_keyboard" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
<Button
|
||||
android:id="@+id/new_session_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<com.google.android.material.textview.MaterialTextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<TextView 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.termux.extrakeys.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<com.termux.shared.terminal.io.extrakeys.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/terminal_toolbar_extra_keys"
|
||||
style="?android:attr/buttonBarStyle"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
|||
|
|
@ -2,5 +2,4 @@
|
|||
<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,5 +2,4 @@
|
|||
<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>
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<attr name="termuxActivityDrawerBackground" format="reference" />
|
||||
<attr name="termuxActivityDrawerImageTint" format="reference" />
|
||||
</resources>
|
||||
|
|
@ -33,11 +33,7 @@
|
|||
<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:\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>
|
||||
under any path other than %1$s.</string>
|
||||
|
||||
|
||||
|
||||
|
|
@ -104,19 +100,14 @@
|
|||
|
||||
|
||||
<!-- TermuxService -->
|
||||
<string name="error_display_over_other_apps_permission_not_granted_to_start_terminal">&TERMUX_APP_NAME; requires
|
||||
<string name="error_display_over_other_apps_permission_not_granted">&TERMUX_APP_NAME; requires
|
||||
\"Display over other apps\" permission to start terminal sessions from background on Android >= 10.
|
||||
Grants it from Settings -> Apps -> &TERMUX_APP_NAME; -> Advanced</string>
|
||||
<string name="error_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>
|
||||
|
||||
|
|
@ -130,8 +121,8 @@
|
|||
|
||||
|
||||
<!-- Miscellaneous -->
|
||||
<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>
|
||||
<string name="error_allow_external_apps_ungranted">%1$s requires `allow-external-apps`
|
||||
property to be set to `true` in `%2$s` file.</string>
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,52 @@
|
|||
<?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>
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
<?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.termux.data.TermuxUrlUtils;
|
||||
import com.termux.shared.data.UrlUtils;
|
||||
|
||||
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, TermuxUrlUtils.extractUrls(text));
|
||||
Assert.assertEquals(expected, UrlUtils.extractUrls(text));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
package com.termux.app.api.file;
|
||||
|
||||
import com.termux.app.api.file.FileReceiverActivity;
|
||||
package com.termux.filepicker;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
|
@ -11,7 +9,7 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class FileReceiverActivityTest {
|
||||
public class TermuxFileReceiverActivityTest {
|
||||
|
||||
@Test
|
||||
public void testIsSharedTextAnUrl() {
|
||||
|
|
@ -21,7 +19,7 @@ public class FileReceiverActivityTest {
|
|||
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(FileReceiverActivity.isSharedTextAnUrl(url));
|
||||
Assert.assertTrue(TermuxFileReceiverActivity.isSharedTextAnUrl(url));
|
||||
}
|
||||
|
||||
List<String> invalidUrls = new ArrayList<>();
|
||||
|
|
@ -29,7 +27,7 @@ public class FileReceiverActivityTest {
|
|||
invalidUrls.add("");
|
||||
invalidUrls.add(null);
|
||||
for (String url : invalidUrls) {
|
||||
Assert.assertFalse(FileReceiverActivity.isSharedTextAnUrl(url));
|
||||
Assert.assertFalse(TermuxFileReceiverActivity.isSharedTextAnUrl(url));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4,7 +4,7 @@ buildscript {
|
|||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:8.13.2"
|
||||
classpath 'com.android.tools.build:gradle:4.2.2'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -15,3 +15,7 @@ allprojects {
|
|||
maven { url "https://jitpack.io" }
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
---
|
||||
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
|
||||
|
|
@ -20,9 +20,10 @@ org.gradle.jvmargs=-Xmx2048M \
|
|||
--add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED
|
||||
android.useAndroidX=true
|
||||
|
||||
minSdkVersion=21
|
||||
minSdkVersion=24
|
||||
targetSdkVersion=28
|
||||
ndkVersion=29.0.14206865
|
||||
compileSdkVersion=36
|
||||
ndkVersion=22.1.7171670
|
||||
compileSdkVersion=30
|
||||
|
||||
markwonVersion=4.6.2
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,7 +1,5 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/sh
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original authors.
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
|
@ -15,114 +15,81 @@
|
|||
# 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 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/.
|
||||
#
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
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
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
# 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
|
||||
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"'
|
||||
|
||||
# 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 ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
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
|
||||
|
|
@ -131,118 +98,88 @@ Please set the JAVA_HOME variable in your environment to match the
|
|||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
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.
|
||||
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.
|
||||
|
||||
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" && ! "$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
|
||||
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
|
||||
|
||||
# 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.
|
||||
# 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" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
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
|
||||
# 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" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# 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' ' '
|
||||
)" '"$@"'
|
||||
# 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"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
|
|
|||
|
|
@ -13,10 +13,8 @@
|
|||
@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
|
||||
|
|
@ -27,8 +25,7 @@
|
|||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
|
|
@ -43,13 +40,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
|||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
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
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
|
|
@ -59,33 +56,32 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
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
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
: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%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
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%
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
|
|
|||
|
|
@ -1,4 +1,2 @@
|
|||
jdk:
|
||||
- openjdk17
|
||||
env:
|
||||
JITPACK_NDK_VERSION: "29.0.14206865"
|
||||
JITPACK_NDK_VERSION: "21.1.6352462"
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB |
|
|
@ -2,8 +2,6 @@ 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
|
||||
|
||||
|
|
@ -52,13 +50,12 @@ tasks.withType(Test) {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.annotation:annotation:1.9.0"
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
}
|
||||
|
||||
task sourceJar(type: Jar) {
|
||||
from android.sourceSets.main.java.srcDirs
|
||||
archiveClassifier = "sources"
|
||||
classifier "sources"
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
|
|
@ -66,7 +63,7 @@ afterEvaluate {
|
|||
publications {
|
||||
// Creates a Maven publication called "release".
|
||||
release(MavenPublication) {
|
||||
from components.findByName('release')
|
||||
from components.release
|
||||
groupId = 'com.termux'
|
||||
artifactId = 'terminal-emulator'
|
||||
version = '0.118.0'
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
<manifest>
|
||||
<manifest package="com.termux.terminal">
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -227,9 +227,9 @@ public final class KeyHandler {
|
|||
return transformForModifiers("\033[3", keyMode, '~');
|
||||
|
||||
case KEYCODE_PAGE_UP:
|
||||
return transformForModifiers("\033[5", keyMode, '~');
|
||||
return "\033[5~";
|
||||
case KEYCODE_PAGE_DOWN:
|
||||
return transformForModifiers("\033[6", keyMode, '~');
|
||||
return "\033[6~";
|
||||
case KEYCODE_DEL:
|
||||
String prefix = ((keyMode & KEYMOD_ALT) == 0) ? "" : "\033";
|
||||
// Just do what xterm and gnome-terminal does:
|
||||
|
|
|
|||
|
|
@ -262,9 +262,6 @@ 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;
|
||||
|
|
@ -2525,15 +2522,6 @@ 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();
|
||||
|
|
|
|||
|
|
@ -126,7 +126,6 @@ public final class TerminalSession extends TerminalOutput {
|
|||
int[] processId = new int[1];
|
||||
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);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
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
|
||||
|
|
@ -10,24 +7,22 @@ import androidx.annotation.Nullable;
|
|||
*/
|
||||
public interface TerminalSessionClient {
|
||||
|
||||
void onTextChanged(@NonNull TerminalSession changedSession);
|
||||
void onTextChanged(TerminalSession changedSession);
|
||||
|
||||
void onTitleChanged(@NonNull TerminalSession changedSession);
|
||||
void onTitleChanged(TerminalSession changedSession);
|
||||
|
||||
void onSessionFinished(@NonNull TerminalSession finishedSession);
|
||||
void onSessionFinished(TerminalSession finishedSession);
|
||||
|
||||
void onCopyTextToClipboard(@NonNull TerminalSession session, String text);
|
||||
void onCopyTextToClipboard(TerminalSession session, String text);
|
||||
|
||||
void onPasteTextFromClipboard(@Nullable TerminalSession session);
|
||||
void onPasteTextFromClipboard(TerminalSession session);
|
||||
|
||||
void onBell(@NonNull TerminalSession session);
|
||||
void onBell(TerminalSession session);
|
||||
|
||||
void onColorsChanged(@NonNull TerminalSession session);
|
||||
void onColorsChanged(TerminalSession session);
|
||||
|
||||
void onTerminalCursorStateChange(boolean state);
|
||||
|
||||
void setTerminalShellPid(@NonNull TerminalSession session, int pid);
|
||||
|
||||
|
||||
|
||||
Integer getTerminalCursorStyle();
|
||||
|
|
|
|||
|
|
@ -2,11 +2,10 @@ 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.9.0"
|
||||
implementation "androidx.annotation:annotation:1.3.0"
|
||||
api project(":terminal-emulator")
|
||||
}
|
||||
|
||||
|
|
@ -30,12 +29,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
|
||||
archiveClassifier = "sources"
|
||||
classifier "sources"
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
|
|
@ -43,7 +42,7 @@ afterEvaluate {
|
|||
publications {
|
||||
// Creates a Maven publication called "release".
|
||||
release(MavenPublication) {
|
||||
from components.findByName('release')
|
||||
from components.release
|
||||
groupId = 'com.termux'
|
||||
artifactId = 'terminal-view'
|
||||
version = '0.118.0'
|
||||
|
|
@ -52,4 +51,3 @@ afterEvaluate {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
<manifest>
|
||||
<manifest package="com.termux.view">
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -125,12 +125,6 @@ public final class TerminalView extends View {
|
|||
|
||||
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)
|
||||
|
|
@ -428,7 +422,7 @@ public final class TerminalView extends View {
|
|||
}
|
||||
}
|
||||
|
||||
inputCodePoint(KEY_EVENT_SOURCE_SOFT_KEYBOARD, codePoint, ctrlHeld, false);
|
||||
inputCodePoint(codePoint, ctrlHeld, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -451,29 +445,19 @@ 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;
|
||||
|
||||
if (isSelectingText() || mEmulator.isAutoScrollDisabled()) {
|
||||
|
||||
boolean skipScrolling = false;
|
||||
if (isSelectingText()) {
|
||||
// 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.
|
||||
if (isSelectingText())
|
||||
stopTextSelectionMode();
|
||||
|
||||
if (mEmulator.isAutoScrollDisabled()) {
|
||||
mTopRow = -rowsInHistory;
|
||||
skipScrolling = true;
|
||||
}
|
||||
stopTextSelectionMode();
|
||||
} else {
|
||||
skipScrolling = true;
|
||||
mTopRow -= rowShift;
|
||||
|
|
@ -825,7 +809,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(event.getDeviceId(), mCombiningAccent, controlDown, leftAltDown);
|
||||
inputCodePoint(mCombiningAccent, controlDown, leftAltDown);
|
||||
mCombiningAccent = result & KeyCharacterMap.COMBINING_ACCENT_MASK;
|
||||
} else {
|
||||
if (mCombiningAccent != 0) {
|
||||
|
|
@ -833,7 +817,7 @@ public final class TerminalView extends View {
|
|||
if (combinedChar > 0) result = combinedChar;
|
||||
mCombiningAccent = 0;
|
||||
}
|
||||
inputCodePoint(event.getDeviceId(), result, controlDown, leftAltDown);
|
||||
inputCodePoint(result, controlDown, leftAltDown);
|
||||
}
|
||||
|
||||
if (mCombiningAccent != oldCombiningAccent) invalidate();
|
||||
|
|
@ -841,9 +825,9 @@ public final class TerminalView extends View {
|
|||
return true;
|
||||
}
|
||||
|
||||
public void inputCodePoint(int eventSource, int codePoint, boolean controlDownFromEvent, boolean leftAltDownFromEvent) {
|
||||
public void inputCodePoint(int codePoint, boolean controlDownFromEvent, boolean leftAltDownFromEvent) {
|
||||
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) {
|
||||
mClient.logInfo(LOG_TAG, "inputCodePoint(eventSource=" + eventSource + ", codePoint=" + codePoint + ", controlDownFromEvent=" + controlDownFromEvent + ", leftAltDownFromEvent="
|
||||
mClient.logInfo(LOG_TAG, "inputCodePoint(codePoint=" + codePoint + ", controlDownFromEvent=" + controlDownFromEvent + ", leftAltDownFromEvent="
|
||||
+ leftAltDownFromEvent + ")");
|
||||
}
|
||||
|
||||
|
|
@ -883,22 +867,19 @@ public final class TerminalView extends View {
|
|||
}
|
||||
|
||||
if (codePoint > -1) {
|
||||
// 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;
|
||||
}
|
||||
// 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:
|
||||
|
|
@ -1457,7 +1438,6 @@ 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) {
|
||||
|
|
@ -1466,7 +1446,6 @@ public final class TerminalView extends View {
|
|||
}
|
||||
};
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
private void showFloatingToolbar() {
|
||||
if (getTextSelectionActionMode() != null) {
|
||||
int delay = ViewConfiguration.getDoubleTapTimeout();
|
||||
|
|
@ -1474,7 +1453,6 @@ public final class TerminalView extends View {
|
|||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
void hideFloatingToolbar() {
|
||||
if (getTextSelectionActionMode() != null) {
|
||||
removeCallbacks(mShowFloatingToolbar);
|
||||
|
|
@ -1483,7 +1461,7 @@ public final class TerminalView extends View {
|
|||
}
|
||||
|
||||
public void updateFloatingToolbarVisibility(MotionEvent event) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && getTextSelectionActionMode() != null) {
|
||||
if (getTextSelectionActionMode() != null) {
|
||||
switch (event.getActionMasked()) {
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
hideFloatingToolbar();
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
/*
|
||||
* 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,11 +1,12 @@
|
|||
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;
|
||||
|
|
@ -163,12 +164,6 @@ 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) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ 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;
|
||||
|
|
@ -15,7 +14,6 @@ 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 {
|
||||
|
|
@ -70,18 +68,13 @@ 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);
|
||||
|
||||
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.setEnterTransition(null);
|
||||
mHandle.setExitTransition(null);
|
||||
mHandle.setContentView(this);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,55 @@
|
|||
The `termux-shared` library is released under [MIT](https://opensource.org/licenses/MIT) license.
|
||||
The `termux-shared` library is released under [GPLv3 only](https://www.gnu.org/licenses/gpl-3.0.html) license.
|
||||
|
||||
### Exceptions
|
||||
|
||||
#### [GPLv3 only](https://www.gnu.org/licenses/gpl-3.0.html)
|
||||
#### [MIT License](https://opensource.org/licenses/MIT)
|
||||
|
||||
- [`src/main/java/com/termux/shared/termux/*`](src/main/java/com/termux/shared/termux).
|
||||
- [`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).
|
||||
|
||||
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.
|
||||
- [`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).
|
||||
##
|
||||
|
||||
|
||||
|
|
@ -16,7 +59,7 @@ The `GPLv3 only` license applies to all files unless specifically specified by a
|
|||
##
|
||||
|
||||
|
||||
#### [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
#### [Apache License 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,24 +2,23 @@ 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.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 '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 "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"
|
||||
implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:5.0'
|
||||
|
||||
implementation "androidx.window:window:1.1.0"
|
||||
// Do not increment version higher than 1.0.0-alpha09 since it will break ViewUtils and needs to be looked into
|
||||
// noinspection GradleDependency
|
||||
implementation "androidx.window:window:1.0.0-alpha09"
|
||||
|
||||
// Do not increment version higher than 2.5 or there
|
||||
// will be runtime exceptions on android < 8
|
||||
|
|
@ -27,20 +26,12 @@ 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 {
|
||||
|
|
@ -51,29 +42,20 @@ 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.5"
|
||||
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.2"
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
}
|
||||
|
||||
task sourceJar(type: Jar) {
|
||||
from android.sourceSets.main.java.srcDirs
|
||||
archiveClassifier = "sources"
|
||||
classifier "sources"
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
|
|
@ -81,7 +63,7 @@ afterEvaluate {
|
|||
publications {
|
||||
// Creates a Maven publication called "release".
|
||||
release(MavenPublication) {
|
||||
from components.findByName('release')
|
||||
from components.release
|
||||
groupId = 'com.termux'
|
||||
artifactId = 'termux-shared'
|
||||
version = '0.118.0'
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.termux.shared">
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
LOCAL_PATH:= $(call my-dir)
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_LDLIBS := -llog
|
||||
LOCAL_MODULE := local-socket
|
||||
LOCAL_SRC_FILES := local-socket.cpp
|
||||
include $(BUILD_SHARED_LIBRARY)
|
||||
|
|
@ -1 +0,0 @@
|
|||
APP_STL := c++_static
|
||||
|
|
@ -1,603 +0,0 @@
|
|||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <cerrno>
|
||||
#include <jni.h>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <android/log.h>
|
||||
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
#define LOG_TAG "local-socket"
|
||||
#define JNI_EXCEPTION "jni-exception"
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
/* Convert a jstring to a std:string. */
|
||||
string jstring_to_stdstr(JNIEnv *env, jstring jString) {
|
||||
jclass stringClass = env->FindClass("java/lang/String");
|
||||
jmethodID getBytes = env->GetMethodID(stringClass, "getBytes", "()[B");
|
||||
jbyteArray jStringBytesArray = (jbyteArray) env->CallObjectMethod(jString, getBytes);
|
||||
jsize length = env->GetArrayLength(jStringBytesArray);
|
||||
jbyte* jStringBytes = env->GetByteArrayElements(jStringBytesArray, nullptr);
|
||||
std::string stdString((char *)jStringBytes, length);
|
||||
env->ReleaseByteArrayElements(jStringBytesArray, jStringBytes, JNI_ABORT);
|
||||
return stdString;
|
||||
}
|
||||
|
||||
/* Get characters before first occurrence of the delim in a std:string. */
|
||||
string get_string_till_first_delim(string str, char delim) {
|
||||
if (!str.empty()) {
|
||||
stringstream cmdline_args(str);
|
||||
string tmp;
|
||||
if (getline(cmdline_args, tmp, delim))
|
||||
return tmp;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/* Replace `\0` values with spaces in a std:string. */
|
||||
string replace_null_with_space(string str) {
|
||||
if (str.empty())
|
||||
return "";
|
||||
|
||||
stringstream tokens(str);
|
||||
string tmp;
|
||||
string str_spaced;
|
||||
while (getline(tokens, tmp, '\0')){
|
||||
str_spaced.append(" " + tmp);
|
||||
}
|
||||
|
||||
if (!str_spaced.empty()) {
|
||||
if (str_spaced.front() == ' ')
|
||||
str_spaced.erase(0, 1);
|
||||
}
|
||||
|
||||
return str_spaced;
|
||||
}
|
||||
|
||||
/* Get class name of a jclazz object with a call to `Class.getName()`. */
|
||||
string get_class_name(JNIEnv *env, jclass clazz) {
|
||||
jclass classClass = env->FindClass("java/lang/Class");
|
||||
jmethodID getName = env->GetMethodID(classClass, "getName", "()Ljava/lang/String;");
|
||||
jstring className = (jstring) env->CallObjectMethod(clazz, getName);
|
||||
return jstring_to_stdstr(env, className);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Get /proc/[pid]/cmdline for a process with pid.
|
||||
*
|
||||
* https://manpages.debian.org/testing/manpages/proc.5.en.html
|
||||
*/
|
||||
string get_process_cmdline(const pid_t pid) {
|
||||
string cmdline;
|
||||
char buf[BUFSIZ];
|
||||
size_t len;
|
||||
char procfile[BUFSIZ];
|
||||
sprintf(procfile, "/proc/%d/cmdline", pid);
|
||||
FILE *fp = fopen(procfile, "rb");
|
||||
if (fp) {
|
||||
while ((len = fread(buf, 1, sizeof(buf), fp)) > 0) {
|
||||
cmdline.append(buf, len);
|
||||
}
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
return cmdline;
|
||||
}
|
||||
|
||||
/* Extract process name from /proc/[pid]/cmdline value of a process. */
|
||||
string get_process_name_from_cmdline(string cmdline) {
|
||||
return get_string_till_first_delim(cmdline, '\0');
|
||||
}
|
||||
|
||||
/* Replace `\0` values with spaces in /proc/[pid]/cmdline value of a process. */
|
||||
string get_process_cmdline_spaced(string cmdline) {
|
||||
return replace_null_with_space(cmdline);
|
||||
}
|
||||
|
||||
|
||||
/* Send an ERROR log message to android logcat. */
|
||||
void log_error(string message) {
|
||||
__android_log_write(ANDROID_LOG_ERROR, LOG_TAG, message.c_str());
|
||||
}
|
||||
|
||||
/* Send an WARN log message to android logcat. */
|
||||
void log_warn(string message) {
|
||||
__android_log_write(ANDROID_LOG_WARN, LOG_TAG, message.c_str());
|
||||
}
|
||||
|
||||
/* Get "title: message" formatted string. */
|
||||
string get_title_and_message(JNIEnv *env, jstring title, string message) {
|
||||
if (title)
|
||||
message = jstring_to_stdstr(env, title) + ": " + message;
|
||||
return message;
|
||||
}
|
||||
|
||||
|
||||
/* Convert timespec to milliseconds. */
|
||||
int64_t timespec_to_milliseconds(const struct timespec* const time) {
|
||||
return (((int64_t)time->tv_sec) * 1000) + (((int64_t)time->tv_nsec)/1000000);
|
||||
}
|
||||
|
||||
/* Convert milliseconds to timeval. */
|
||||
timeval milliseconds_to_timeval(int milliseconds) {
|
||||
struct timeval tv = {};
|
||||
tv.tv_sec = milliseconds / 1000;
|
||||
tv.tv_usec = (milliseconds % 1000) * 1000;
|
||||
return tv;
|
||||
}
|
||||
|
||||
|
||||
// Note: Exceptions thrown from JNI must be caught with Throwable class instead of Exception,
|
||||
// otherwise exception will be sent to UncaughtExceptionHandler of the thread.
|
||||
// Android studio complains that getJniResult functions always return nullptr since linter is broken
|
||||
// for jboolean and jobject if comparisons.
|
||||
bool checkJniException(JNIEnv *env) {
|
||||
if (env->ExceptionCheck()) {
|
||||
jthrowable throwable = env->ExceptionOccurred();
|
||||
if (throwable != NULL) {
|
||||
env->ExceptionClear();
|
||||
env->Throw(throwable);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
string getJniResultString(const int retvalParam, const int errnoParam,
|
||||
string errmsgParam, const int intDataParam) {
|
||||
return "retval=" + to_string(retvalParam) + ", errno=" + to_string(errnoParam) +
|
||||
", errmsg=\"" + errmsgParam + "\"" + ", intData=" + to_string(intDataParam);
|
||||
}
|
||||
|
||||
/* Get "com/termux/shared/jni/models/JniResult" object that can be returned as result for a JNI call. */
|
||||
jobject getJniResult(JNIEnv *env, jstring title, const int retvalParam, const int errnoParam,
|
||||
string errmsgParam, const int intDataParam) {
|
||||
jclass clazz = env->FindClass("com/termux/shared/jni/models/JniResult");
|
||||
if (checkJniException(env)) return NULL;
|
||||
if (!clazz) {
|
||||
log_error(get_title_and_message(env, title,
|
||||
"Failed to find JniResult class to create object for " +
|
||||
getJniResultString(retvalParam, errnoParam, errmsgParam, intDataParam)));
|
||||
return NULL;
|
||||
}
|
||||
|
||||
jmethodID constructor = env->GetMethodID(clazz, "<init>", "(IILjava/lang/String;I)V");
|
||||
if (checkJniException(env)) return NULL;
|
||||
if (!constructor) {
|
||||
log_error(get_title_and_message(env, title,
|
||||
"Failed to get constructor for JniResult class to create object for " +
|
||||
getJniResultString(retvalParam, errnoParam, errmsgParam, intDataParam)));
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!errmsgParam.empty())
|
||||
errmsgParam = get_title_and_message(env, title, string(errmsgParam));
|
||||
|
||||
jobject obj = env->NewObject(clazz, constructor, retvalParam, errnoParam, env->NewStringUTF(errmsgParam.c_str()), intDataParam);
|
||||
if (checkJniException(env)) return NULL;
|
||||
if (obj == NULL) {
|
||||
log_error(get_title_and_message(env, title,
|
||||
"Failed to get JniResult object for " +
|
||||
getJniResultString(retvalParam, errnoParam, errmsgParam, intDataParam)));
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
jobject getJniResult(JNIEnv *env, jstring title, const int retvalParam, const int errnoParam) {
|
||||
return getJniResult(env, title, retvalParam, errnoParam, strerror(errnoParam), 0);
|
||||
}
|
||||
|
||||
jobject getJniResult(JNIEnv *env, jstring title, const int retvalParam, string errmsgPrefixParam) {
|
||||
return getJniResult(env, title, retvalParam, 0, errmsgPrefixParam, 0);
|
||||
}
|
||||
|
||||
jobject getJniResult(JNIEnv *env, jstring title, const int retvalParam, const int errnoParam, string errmsgPrefixParam) {
|
||||
return getJniResult(env, title, retvalParam, errnoParam, errmsgPrefixParam + ": " + string(strerror(errnoParam)), 0);
|
||||
}
|
||||
|
||||
jobject getJniResult(JNIEnv *env, jstring title, const int intDataParam) {
|
||||
return getJniResult(env, title, 0, 0, "", intDataParam);
|
||||
}
|
||||
|
||||
jobject getJniResult(JNIEnv *env, jstring title) {
|
||||
return getJniResult(env, title, 0, 0, "", 0);
|
||||
}
|
||||
|
||||
|
||||
/* Set int fieldName field for clazz to value. */
|
||||
string setIntField(JNIEnv *env, jobject obj, jclass clazz, const string fieldName, const int value) {
|
||||
jfieldID field = env->GetFieldID(clazz, fieldName.c_str(), "I");
|
||||
if (checkJniException(env)) return JNI_EXCEPTION;
|
||||
if (!field) {
|
||||
return "Failed to get int \"" + string(fieldName) + "\" field of \"" +
|
||||
get_class_name(env, clazz) + "\" class to set value \"" + to_string(value) + "\"";
|
||||
}
|
||||
|
||||
env->SetIntField(obj, field, value);
|
||||
if (checkJniException(env)) return JNI_EXCEPTION;
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/* Set String fieldName field for clazz to value. */
|
||||
string setStringField(JNIEnv *env, jobject obj, jclass clazz, const string fieldName, const string value) {
|
||||
jfieldID field = env->GetFieldID(clazz, fieldName.c_str(), "Ljava/lang/String;");
|
||||
if (checkJniException(env)) return JNI_EXCEPTION;
|
||||
if (!field) {
|
||||
return "Failed to get String \"" + string(fieldName) + "\" field of \"" +
|
||||
get_class_name(env, clazz) + "\" class to set value \"" + value + "\"";
|
||||
}
|
||||
|
||||
env->SetObjectField(obj, field, env->NewStringUTF(value.c_str()));
|
||||
if (checkJniException(env)) return JNI_EXCEPTION;
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_com_termux_shared_net_socket_local_LocalSocketManager_createServerSocketNative(JNIEnv *env, jclass clazz,
|
||||
jstring logTitle,
|
||||
jbyteArray pathArray,
|
||||
jint backlog) {
|
||||
if (backlog < 1 || backlog > 500) {
|
||||
return getJniResult(env, logTitle, -1, "createServerSocketNative(): Backlog \"" +
|
||||
to_string(backlog) + "\" is not between 1-500");
|
||||
}
|
||||
|
||||
// Create server socket
|
||||
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd == -1) {
|
||||
return getJniResult(env, logTitle, -1, errno, "createServerSocketNative(): Create local socket failed");
|
||||
}
|
||||
|
||||
jbyte* path = env->GetByteArrayElements(pathArray, nullptr);
|
||||
if (checkJniException(env)) return NULL;
|
||||
if (path == nullptr) {
|
||||
close(fd);
|
||||
return getJniResult(env, logTitle, -1, "createServerSocketNative(): Path passed is null");
|
||||
}
|
||||
|
||||
// On Linux, sun_path is 108 bytes (UNIX_PATH_MAX) in size
|
||||
int chars = env->GetArrayLength(pathArray);
|
||||
if (checkJniException(env)) return NULL;
|
||||
if (chars >= 108 || chars >= sizeof(struct sockaddr_un) - sizeof(sa_family_t)) {
|
||||
env->ReleaseByteArrayElements(pathArray, path, JNI_ABORT);
|
||||
if (checkJniException(env)) return NULL;
|
||||
close(fd);
|
||||
return getJniResult(env, logTitle, -1, "createServerSocketNative(): Path passed is too long");
|
||||
}
|
||||
|
||||
struct sockaddr_un adr = {.sun_family = AF_UNIX};
|
||||
memcpy(&adr.sun_path, path, chars);
|
||||
|
||||
// Bind path to server socket
|
||||
if (::bind(fd, reinterpret_cast<struct sockaddr*>(&adr), sizeof(adr)) == -1) {
|
||||
int errnoBackup = errno;
|
||||
env->ReleaseByteArrayElements(pathArray, path, JNI_ABORT);
|
||||
if (checkJniException(env)) return NULL;
|
||||
close(fd);
|
||||
return getJniResult(env, logTitle, -1, errnoBackup,
|
||||
"createServerSocketNative(): Bind to local socket at path \"" + string(adr.sun_path) + "\" with fd " + to_string(fd) + " failed");
|
||||
}
|
||||
|
||||
// Start listening for client sockets on server socket
|
||||
if (listen(fd, backlog) == -1) {
|
||||
int errnoBackup = errno;
|
||||
env->ReleaseByteArrayElements(pathArray, path, JNI_ABORT);
|
||||
if (checkJniException(env)) return NULL;
|
||||
close(fd);
|
||||
return getJniResult(env, logTitle, -1, errnoBackup,
|
||||
"createServerSocketNative(): Listen on local socket at path \"" + string(adr.sun_path) + "\" with fd " + to_string(fd) + " failed");
|
||||
}
|
||||
|
||||
env->ReleaseByteArrayElements(pathArray, path, JNI_ABORT);
|
||||
if (checkJniException(env)) return NULL;
|
||||
|
||||
// Return success and server socket fd in JniResult.intData field
|
||||
return getJniResult(env, logTitle, fd);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_com_termux_shared_net_socket_local_LocalSocketManager_closeSocketNative(JNIEnv *env, jclass clazz,
|
||||
jstring logTitle, jint fd) {
|
||||
if (fd < 0) {
|
||||
return getJniResult(env, logTitle, -1, "closeSocketNative(): Invalid fd \"" + to_string(fd) + "\" passed");
|
||||
}
|
||||
|
||||
if (close(fd) == -1) {
|
||||
return getJniResult(env, logTitle, -1, errno, "closeSocketNative(): Failed to close socket fd " + to_string(fd));
|
||||
}
|
||||
|
||||
// Return success
|
||||
return getJniResult(env, logTitle);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_com_termux_shared_net_socket_local_LocalSocketManager_acceptNative(JNIEnv *env, jclass clazz,
|
||||
jstring logTitle, jint fd) {
|
||||
if (fd < 0) {
|
||||
return getJniResult(env, logTitle, -1, "acceptNative(): Invalid fd \"" + to_string(fd) + "\" passed");
|
||||
}
|
||||
|
||||
// Accept client socket
|
||||
int clientFd = accept(fd, nullptr, nullptr);
|
||||
if (clientFd == -1) {
|
||||
return getJniResult(env, logTitle, -1, errno, "acceptNative(): Failed to accept client on fd " + to_string(fd));
|
||||
}
|
||||
|
||||
// Return success and client socket fd in JniResult.intData field
|
||||
return getJniResult(env, logTitle, clientFd);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_com_termux_shared_net_socket_local_LocalSocketManager_readNative(JNIEnv *env, jclass clazz,
|
||||
jstring logTitle,
|
||||
jint fd, jbyteArray dataArray,
|
||||
jlong deadline) {
|
||||
if (fd < 0) {
|
||||
return getJniResult(env, logTitle, -1, "readNative(): Invalid fd \"" + to_string(fd) + "\" passed");
|
||||
}
|
||||
|
||||
jbyte* data = env->GetByteArrayElements(dataArray, nullptr);
|
||||
if (checkJniException(env)) return NULL;
|
||||
if (data == nullptr) {
|
||||
return getJniResult(env, logTitle, -1, "readNative(): data passed is null");
|
||||
}
|
||||
|
||||
struct timespec time = {};
|
||||
jbyte* current = data;
|
||||
int bytes = env->GetArrayLength(dataArray);
|
||||
if (checkJniException(env)) return NULL;
|
||||
int bytesRead = 0;
|
||||
while (bytesRead < bytes) {
|
||||
if (deadline > 0) {
|
||||
if (clock_gettime(CLOCK_REALTIME, &time) != -1) {
|
||||
// If current time is greater than the time defined in deadline
|
||||
if (timespec_to_milliseconds(&time) > deadline) {
|
||||
env->ReleaseByteArrayElements(dataArray, data, 0);
|
||||
if (checkJniException(env)) return NULL;
|
||||
return getJniResult(env, logTitle, -1,
|
||||
"readNative(): Deadline \"" + to_string(deadline) + "\" timeout");
|
||||
}
|
||||
} else {
|
||||
log_warn(get_title_and_message(env, logTitle,
|
||||
"readNative(): Deadline \"" + to_string(deadline) +
|
||||
"\" timeout will not work since failed to get current time"));
|
||||
}
|
||||
}
|
||||
|
||||
// Read data from socket
|
||||
int ret = read(fd, current, bytes);
|
||||
if (ret == -1) {
|
||||
int errnoBackup = errno;
|
||||
env->ReleaseByteArrayElements(dataArray, data, 0);
|
||||
if (checkJniException(env)) return NULL;
|
||||
return getJniResult(env, logTitle, -1, errnoBackup, "readNative(): Failed to read on fd " + to_string(fd));
|
||||
}
|
||||
// EOF, peer closed writing end
|
||||
if (ret == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
bytesRead += ret;
|
||||
current += ret;
|
||||
}
|
||||
|
||||
env->ReleaseByteArrayElements(dataArray, data, 0);
|
||||
if (checkJniException(env)) return NULL;
|
||||
|
||||
// Return success and bytes read in JniResult.intData field
|
||||
return getJniResult(env, logTitle, bytesRead);
|
||||
}
|
||||
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_com_termux_shared_net_socket_local_LocalSocketManager_sendNative(JNIEnv *env, jclass clazz,
|
||||
jstring logTitle,
|
||||
jint fd, jbyteArray dataArray,
|
||||
jlong deadline) {
|
||||
if (fd < 0) {
|
||||
return getJniResult(env, logTitle, -1, "sendNative(): Invalid fd \"" + to_string(fd) + "\" passed");
|
||||
}
|
||||
|
||||
jbyte* data = env->GetByteArrayElements(dataArray, nullptr);
|
||||
if (checkJniException(env)) return NULL;
|
||||
if (data == nullptr) {
|
||||
return getJniResult(env, logTitle, -1, "sendNative(): data passed is null");
|
||||
}
|
||||
|
||||
struct timespec time = {};
|
||||
jbyte* current = data;
|
||||
int bytes = env->GetArrayLength(dataArray);
|
||||
if (checkJniException(env)) return NULL;
|
||||
while (bytes > 0) {
|
||||
if (deadline > 0) {
|
||||
if (clock_gettime(CLOCK_REALTIME, &time) != -1) {
|
||||
// If current time is greater than the time defined in deadline
|
||||
if (timespec_to_milliseconds(&time) > deadline) {
|
||||
env->ReleaseByteArrayElements(dataArray, data, JNI_ABORT);
|
||||
if (checkJniException(env)) return NULL;
|
||||
return getJniResult(env, logTitle, -1,
|
||||
"sendNative(): Deadline \"" + to_string(deadline) + "\" timeout");
|
||||
}
|
||||
} else {
|
||||
log_warn(get_title_and_message(env, logTitle,
|
||||
"sendNative(): Deadline \"" + to_string(deadline) +
|
||||
"\" timeout will not work since failed to get current time"));
|
||||
}
|
||||
}
|
||||
|
||||
// Send data to socket
|
||||
int ret = send(fd, current, bytes, MSG_NOSIGNAL);
|
||||
if (ret == -1) {
|
||||
int errnoBackup = errno;
|
||||
env->ReleaseByteArrayElements(dataArray, data, JNI_ABORT);
|
||||
if (checkJniException(env)) return NULL;
|
||||
return getJniResult(env, logTitle, -1, errnoBackup, "sendNative(): Failed to send on fd " + to_string(fd));
|
||||
}
|
||||
|
||||
bytes -= ret;
|
||||
current += ret;
|
||||
}
|
||||
|
||||
env->ReleaseByteArrayElements(dataArray, data, JNI_ABORT);
|
||||
if (checkJniException(env)) return NULL;
|
||||
|
||||
// Return success
|
||||
return getJniResult(env, logTitle);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_com_termux_shared_net_socket_local_LocalSocketManager_availableNative(JNIEnv *env, jclass clazz,
|
||||
jstring logTitle, jint fd) {
|
||||
if (fd < 0) {
|
||||
return getJniResult(env, logTitle, -1, "availableNative(): Invalid fd \"" + to_string(fd) + "\" passed");
|
||||
}
|
||||
|
||||
int available = 0;
|
||||
if (ioctl(fd, SIOCINQ, &available) == -1) {
|
||||
return getJniResult(env, logTitle, -1, errno,
|
||||
"availableNative(): Failed to get number of unread bytes in the receive buffer of fd " + to_string(fd));
|
||||
}
|
||||
|
||||
// Return success and bytes available in JniResult.intData field
|
||||
return getJniResult(env, logTitle, available);
|
||||
}
|
||||
|
||||
/* Sets socket option timeout in milliseconds. */
|
||||
int set_socket_timeout(int fd, int option, int timeout) {
|
||||
struct timeval tv = milliseconds_to_timeval(timeout);
|
||||
socklen_t len = sizeof(tv);
|
||||
return setsockopt(fd, SOL_SOCKET, option, &tv, len);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_com_termux_shared_net_socket_local_LocalSocketManager_setSocketReadTimeoutNative(JNIEnv *env, jclass clazz,
|
||||
jstring logTitle,
|
||||
jint fd, jint timeout) {
|
||||
if (fd < 0) {
|
||||
return getJniResult(env, logTitle, -1, "setSocketReadTimeoutNative(): Invalid fd \"" + to_string(fd) + "\" passed");
|
||||
}
|
||||
|
||||
if (set_socket_timeout(fd, SO_RCVTIMEO, timeout) == -1) {
|
||||
return getJniResult(env, logTitle, -1, errno,
|
||||
"setSocketReadTimeoutNative(): Failed to set socket receiving (SO_RCVTIMEO) timeout for fd " + to_string(fd));
|
||||
}
|
||||
|
||||
// Return success
|
||||
return getJniResult(env, logTitle);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_com_termux_shared_net_socket_local_LocalSocketManager_setSocketSendTimeoutNative(JNIEnv *env, jclass clazz,
|
||||
jstring logTitle,
|
||||
jint fd, jint timeout) {
|
||||
if (fd < 0) {
|
||||
return getJniResult(env, logTitle, -1, "setSocketSendTimeoutNative(): Invalid fd \"" +
|
||||
to_string(fd) + "\" passed");
|
||||
}
|
||||
|
||||
if (set_socket_timeout(fd, SO_SNDTIMEO, timeout) == -1) {
|
||||
return getJniResult(env, logTitle, -1, errno,
|
||||
"setSocketSendTimeoutNative(): Failed to set socket sending (SO_SNDTIMEO) timeout for fd " + to_string(fd));
|
||||
}
|
||||
|
||||
// Return success
|
||||
return getJniResult(env, logTitle);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_com_termux_shared_net_socket_local_LocalSocketManager_getPeerCredNative(JNIEnv *env, jclass clazz,
|
||||
jstring logTitle,
|
||||
jint fd, jobject peerCred) {
|
||||
if (fd < 0) {
|
||||
return getJniResult(env, logTitle, -1, "getPeerCredNative(): Invalid fd \"" + to_string(fd) + "\" passed");
|
||||
}
|
||||
|
||||
if (peerCred == nullptr) {
|
||||
return getJniResult(env, logTitle, -1, "getPeerCredNative(): peerCred passed is null");
|
||||
}
|
||||
|
||||
// Initialize to -1 instead of 0 in case a failed getsockopt() call somehow doesn't report failure and returns the uid of root
|
||||
struct ucred cred = {};
|
||||
cred.pid = -1; cred.uid = -1; cred.gid = -1;
|
||||
|
||||
socklen_t len = sizeof(cred);
|
||||
|
||||
if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) == -1) {
|
||||
return getJniResult(env, logTitle, -1, errno, "getPeerCredNative(): Failed to get peer credentials for fd " + to_string(fd));
|
||||
}
|
||||
|
||||
// Fill "com.termux.shared.net.socket.local.PeerCred" object.
|
||||
// The pid, uid and gid will always be set based on ucred.
|
||||
// The pname and cmdline will only be set if current process has access to "/proc/[pid]/cmdline"
|
||||
// of peer process. Processes of other users/apps are not normally accessible.
|
||||
jclass peerCredClazz = env->GetObjectClass(peerCred);
|
||||
if (checkJniException(env)) return NULL;
|
||||
if (!peerCredClazz) {
|
||||
return getJniResult(env, logTitle, -1, errno, "getPeerCredNative(): Failed to get PeerCred class");
|
||||
}
|
||||
|
||||
string error;
|
||||
|
||||
error = setIntField(env, peerCred, peerCredClazz, "pid", cred.pid);
|
||||
if (!error.empty()) {
|
||||
if (error == JNI_EXCEPTION) return NULL;
|
||||
return getJniResult(env, logTitle, -1, "getPeerCredNative(): " + error);
|
||||
}
|
||||
|
||||
error = setIntField(env, peerCred, peerCredClazz, "uid", cred.uid);
|
||||
if (!error.empty()) {
|
||||
if (error == JNI_EXCEPTION) return NULL;
|
||||
return getJniResult(env, logTitle, -1, "getPeerCredNative(): " + error);
|
||||
}
|
||||
|
||||
error = setIntField(env, peerCred, peerCredClazz, "gid", cred.gid);
|
||||
if (!error.empty()) {
|
||||
if (error == JNI_EXCEPTION) return NULL;
|
||||
return getJniResult(env, logTitle, -1, "getPeerCredNative(): " + error);
|
||||
}
|
||||
|
||||
string cmdline = get_process_cmdline(cred.pid);
|
||||
if (!cmdline.empty()) {
|
||||
error = setStringField(env, peerCred, peerCredClazz, "pname", get_process_name_from_cmdline(cmdline));
|
||||
if (!error.empty()) {
|
||||
if (error == JNI_EXCEPTION) return NULL;
|
||||
return getJniResult(env, logTitle, -1, "getPeerCredNative(): " + error);
|
||||
}
|
||||
|
||||
error = setStringField(env, peerCred, peerCredClazz, "cmdline", get_process_cmdline_spaced(cmdline));
|
||||
if (!error.empty()) {
|
||||
if (error == JNI_EXCEPTION) return NULL;
|
||||
return getJniResult(env, logTitle, -1, "getPeerCredNative(): " + error);
|
||||
}
|
||||
}
|
||||
|
||||
// Return success since PeerCred was filled successfully
|
||||
return getJniResult(env, logTitle);
|
||||
}
|
||||
|
|
@ -17,17 +17,15 @@ import android.view.MenuInflater;
|
|||
import android.view.MenuItem;
|
||||
|
||||
import com.termux.shared.R;
|
||||
import com.termux.shared.activity.media.AppCompatActivityUtils;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.file.filesystem.FileType;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.errors.Error;
|
||||
import com.termux.shared.models.errors.Error;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.interact.ShareUtils;
|
||||
import com.termux.shared.models.ReportInfo;
|
||||
import com.termux.shared.theme.NightMode;
|
||||
|
||||
import org.commonmark.node.FencedCodeBlock;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
|
@ -74,8 +72,6 @@ public class ReportActivity extends AppCompatActivity {
|
|||
super.onCreate(savedInstanceState);
|
||||
Logger.logVerbose(LOG_TAG, "onCreate");
|
||||
|
||||
AppCompatActivityUtils.setNightMode(this, NightMode.getAppNightMode().getName(), true);
|
||||
|
||||
setContentView(R.layout.activity_report);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
package com.termux.shared.activity;
|
||||
|
||||
import com.termux.shared.errors.Errno;
|
||||
|
||||
public class ActivityErrno extends Errno {
|
||||
|
||||
public static final String TYPE = "Activity Error";
|
||||
|
||||
|
||||
/* Errors for starting activities (100-150) */
|
||||
public static final Errno ERRNO_START_ACTIVITY_FAILED_WITH_EXCEPTION = new Errno(TYPE, 100, "Failed to start \"%1$s\" activity.\nException: %2$s");
|
||||
public static final Errno ERRNO_START_ACTIVITY_FOR_RESULT_FAILED_WITH_EXCEPTION = new Errno(TYPE, 101, "Failed to start \"%1$s\" activity for result.\nException: %2$s");
|
||||
public static final Errno ERRNO_STARTING_ACTIVITY_WITH_NULL_CONTEXT = new Errno(TYPE, 102, "Cannot start \"%1$s\" activity with null Context");
|
||||
|
||||
|
||||
ActivityErrno(final String type, final int code, final String message) {
|
||||
super(type, code, message);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,137 +0,0 @@
|
|||
package com.termux.shared.activity;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.termux.shared.errors.Error;
|
||||
import com.termux.shared.errors.FunctionErrno;
|
||||
|
||||
|
||||
public class ActivityUtils {
|
||||
|
||||
private static final String LOG_TAG = "ActivityUtils";
|
||||
|
||||
/**
|
||||
* Wrapper for {@link #startActivity(Context, Intent, boolean, boolean)}.
|
||||
*/
|
||||
public static Error startActivity(@NonNull Context context, @NonNull Intent intent) {
|
||||
return startActivity(context, intent, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start an {@link Activity}.
|
||||
*
|
||||
* @param context The context for operations.
|
||||
* @param intent The {@link Intent} to send to start the activity.
|
||||
* @param logErrorMessage If an error message should be logged if failed to start activity.
|
||||
* @param showErrorMessage If an error message toast should be shown if failed to start activity
|
||||
* in addition to logging a message. The {@code context} must not be
|
||||
* {@code null}.
|
||||
* @return Returns the {@code error} if starting activity was not successful, otherwise {@code null}.
|
||||
*/
|
||||
public static Error startActivity(Context context, @NonNull Intent intent,
|
||||
boolean logErrorMessage, boolean showErrorMessage) {
|
||||
Error error;
|
||||
String activityName = intent.getComponent() != null ? intent.getComponent().getClassName() : "Unknown";
|
||||
|
||||
if (context == null) {
|
||||
error = ActivityErrno.ERRNO_STARTING_ACTIVITY_WITH_NULL_CONTEXT.getError(activityName);
|
||||
if (logErrorMessage)
|
||||
error.logErrorAndShowToast(null, LOG_TAG);
|
||||
return error;
|
||||
}
|
||||
|
||||
try {
|
||||
context.startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
error = ActivityErrno.ERRNO_START_ACTIVITY_FAILED_WITH_EXCEPTION.getError(e, activityName, e.getMessage());
|
||||
if (logErrorMessage)
|
||||
error.logErrorAndShowToast(showErrorMessage ? context : null, LOG_TAG);
|
||||
return error;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Wrapper for {@link #startActivityForResult(Context, int, Intent, boolean, boolean, ActivityResultLauncher)}.
|
||||
*/
|
||||
public static Error startActivityForResult(Context context, int requestCode, @NonNull Intent intent) {
|
||||
return startActivityForResult(context, requestCode, intent, true, true, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for {@link #startActivityForResult(Context, int, Intent, boolean, boolean, ActivityResultLauncher)}.
|
||||
*/
|
||||
public static Error startActivityForResult(Context context, int requestCode, @NonNull Intent intent,
|
||||
boolean logErrorMessage, boolean showErrorMessage) {
|
||||
return startActivityForResult(context, requestCode, intent, logErrorMessage, showErrorMessage, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start an {@link Activity} for result.
|
||||
*
|
||||
* @param context The context for operations. It must be an instance of {@link Activity} or
|
||||
* {@link AppCompatActivity}. It is ignored if {@code activityResultLauncher}
|
||||
* is not {@code null}.
|
||||
* @param requestCode The request code to use while sending intent. This must be >= 0, otherwise
|
||||
* exception will be raised. This is ignored if {@code activityResultLauncher}
|
||||
* is {@code null}.
|
||||
* @param intent The {@link Intent} to send to start the activity.
|
||||
* @param logErrorMessage If an error message should be logged if failed to start activity.
|
||||
* @param showErrorMessage If an error message toast should be shown if failed to start activity
|
||||
* in addition to logging a message. The {@code context} must not be
|
||||
* {@code null}.
|
||||
* @param activityResultLauncher The {@link ActivityResultLauncher<Intent>} to use for start the
|
||||
* activity. If this is {@code null}, then
|
||||
* {@link Activity#startActivityForResult(Intent, int)} will be
|
||||
* used instead.
|
||||
* Note that later is deprecated.
|
||||
* @return Returns the {@code error} if starting activity was not successful, otherwise {@code null}.
|
||||
*/
|
||||
public static Error startActivityForResult(Context context, int requestCode, @NonNull Intent intent,
|
||||
boolean logErrorMessage, boolean showErrorMessage,
|
||||
@Nullable ActivityResultLauncher<Intent> activityResultLauncher) {
|
||||
Error error;
|
||||
String activityName = intent.getComponent() != null ? intent.getComponent().getClassName() : "Unknown";
|
||||
try {
|
||||
if (activityResultLauncher != null) {
|
||||
activityResultLauncher.launch(intent);
|
||||
} else {
|
||||
if (context == null) {
|
||||
error = ActivityErrno.ERRNO_STARTING_ACTIVITY_WITH_NULL_CONTEXT.getError(activityName);
|
||||
if (logErrorMessage)
|
||||
error.logErrorAndShowToast(null, LOG_TAG);
|
||||
return error;
|
||||
}
|
||||
|
||||
if (context instanceof AppCompatActivity)
|
||||
((AppCompatActivity) context).startActivityForResult(intent, requestCode);
|
||||
else if (context instanceof Activity)
|
||||
((Activity) context).startActivityForResult(intent, requestCode);
|
||||
else {
|
||||
error = FunctionErrno.ERRNO_PARAMETER_NOT_INSTANCE_OF.getError("context", "startActivityForResult", "Activity or AppCompatActivity");
|
||||
if (logErrorMessage)
|
||||
error.logErrorAndShowToast(showErrorMessage ? context : null, LOG_TAG);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
error = ActivityErrno.ERRNO_START_ACTIVITY_FOR_RESULT_FAILED_WITH_EXCEPTION.getError(e, activityName, e.getMessage());
|
||||
if (logErrorMessage)
|
||||
error.logErrorAndShowToast(showErrorMessage ? context : null, LOG_TAG);
|
||||
return error;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
package com.termux.shared.activity.media;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StyleRes;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.theme.NightMode;
|
||||
|
||||
public class AppCompatActivityUtils {
|
||||
|
||||
private static final String LOG_TAG = "AppCompatActivityUtils";
|
||||
|
||||
|
||||
/** Set activity night mode.
|
||||
*
|
||||
* @param activity The host {@link AppCompatActivity}.
|
||||
* @param name The {@link String} representing the name for a {@link NightMode}.
|
||||
* @param local If set to {@code true}, then a call to {@link AppCompatDelegate#setLocalNightMode(int)}
|
||||
* will be made, otherwise to {@link AppCompatDelegate#setDefaultNightMode(int)}.
|
||||
*/
|
||||
public static void setNightMode(AppCompatActivity activity, String name, boolean local) {
|
||||
if (name == null) return;
|
||||
NightMode nightMode = NightMode.modeOf(name);
|
||||
if (nightMode != null) {
|
||||
if (local) {
|
||||
if (activity != null) {
|
||||
activity.getDelegate().setLocalNightMode(nightMode.getMode());
|
||||
}
|
||||
} else {
|
||||
AppCompatDelegate.setDefaultNightMode(nightMode.getMode());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** Set activity toolbar.
|
||||
*
|
||||
* @param activity The host {@link AppCompatActivity}.
|
||||
* @param id The toolbar resource id.
|
||||
*/
|
||||
public static void setToolbar(@NonNull AppCompatActivity activity, @IdRes int id) {
|
||||
Toolbar toolbar = activity.findViewById(id);
|
||||
if (toolbar != null)
|
||||
activity.setSupportActionBar(toolbar);
|
||||
}
|
||||
|
||||
/** Set activity toolbar title.
|
||||
*
|
||||
* @param activity The host {@link AppCompatActivity}.
|
||||
* @param id The toolbar resource id.
|
||||
* @param title The toolbar title {@link String}.
|
||||
* @param titleAppearance The toolbar title TextAppearance resource id.
|
||||
*/
|
||||
public static void setToolbarTitle(@NonNull AppCompatActivity activity, @IdRes int id,
|
||||
String title, @StyleRes int titleAppearance) {
|
||||
Toolbar toolbar = activity.findViewById(id);
|
||||
if (toolbar != null) {
|
||||
//toolbar.setTitle(title); // Does not work
|
||||
final ActionBar actionBar = activity.getSupportActionBar();
|
||||
if (actionBar != null)
|
||||
actionBar.setTitle(title);
|
||||
|
||||
try {
|
||||
if (titleAppearance != 0)
|
||||
toolbar.setTitleTextAppearance(activity, titleAppearance);
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to set toolbar title appearance to style resource id " + titleAppearance, e);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/** Set activity toolbar subtitle.
|
||||
*
|
||||
* @param activity The host {@link AppCompatActivity}.
|
||||
* @param id The toolbar resource id.
|
||||
* @param subtitle The toolbar subtitle {@link String}.
|
||||
* @param subtitleAppearance The toolbar subtitle TextAppearance resource id.
|
||||
*/
|
||||
public static void setToolbarSubtitle(@NonNull AppCompatActivity activity, @IdRes int id,
|
||||
String subtitle, @StyleRes int subtitleAppearance) {
|
||||
Toolbar toolbar = activity.findViewById(id);
|
||||
if (toolbar != null) {
|
||||
toolbar.setSubtitle(subtitle);
|
||||
try {
|
||||
if (subtitleAppearance != 0)
|
||||
toolbar.setSubtitleTextAppearance(activity, subtitleAppearance);
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to set toolbar subtitle appearance to style resource id " + subtitleAppearance, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Set whether back button should be shown in activity toolbar.
|
||||
*
|
||||
* @param activity The host {@link AppCompatActivity}.
|
||||
* @param showBackButtonInActionBar Set to {@code true} to enable and {@code false} to disable.
|
||||
*/
|
||||
public static void setShowBackButtonInActionBar(@NonNull AppCompatActivity activity,
|
||||
boolean showBackButtonInActionBar) {
|
||||
final ActionBar actionBar = activity.getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
if (showBackButtonInActionBar) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setDisplayShowHomeEnabled(true);
|
||||
} else {
|
||||
actionBar.setDisplayHomeAsUpEnabled(false);
|
||||
actionBar.setDisplayShowHomeEnabled(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
package com.termux.shared.android;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.reflection.ReflectionUtils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Utils for Developer Options -> Feature Flags. The page won't show in user/production builds and
|
||||
* is only shown in userdebug builds.
|
||||
* https://cs.android.com/android/_/android/platform/frameworks/base/+/09dcdad5ebc159861920f090e07da60fac71ac0a:core/java/android/util/FeatureFlagUtils.java
|
||||
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r31:packages/apps/Settings/src/com/android/settings/development/featureflags/FeatureFlagsPreferenceController.java;l=42
|
||||
*
|
||||
* The feature flags value can be modified in two ways.
|
||||
*
|
||||
* 1. sysprops with `setprop` command with root. Will be unset by default.
|
||||
* Set value: `setprop persist.sys.fflag.override.settings_enable_monitor_phantom_procs false`
|
||||
* Get value: `getprop persist.sys.fflag.override.settings_enable_monitor_phantom_procs`
|
||||
* Unset value: `setprop persist.sys.fflag.override.settings_enable_monitor_phantom_procs ""`
|
||||
* Running `setprop` command requires root and even adb `shell` user cannot modify the values
|
||||
* since selinux will not allow it by default. Some props like `settings_dynamic_system` can be
|
||||
* set since they are exempted for `shell` in sepolicy.
|
||||
*
|
||||
* init: Unable to set property 'persist.sys.fflag.override.settings_enable_monitor_phantom_procs' from uid:2000 gid:2000 pid:9576: SELinux permission check failed
|
||||
* [ 1034.877067] type=1107 audit(1644436809.637:34): uid=0 auid=4294967295 ses=4294967295 subj=u:r:init:s0 msg='avc: denied { set } for property=persist.sys.fflag.override.settings_enable_monitor_phantom_procs pid=9576 uid=2000 gid=2000 scontext=u:r:shell:s0 tcontext=u:object_r:system_prop:s0 tclass=property_service permissive=0'
|
||||
*
|
||||
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r4:system/sepolicy/private/property_contexts;l=71
|
||||
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r4:system/sepolicy/private/shell.te;l=149
|
||||
*
|
||||
* 2. settings global list with adb or root. Will be unset by default. This takes precedence over
|
||||
* sysprop value since `FeatureFlagUtils.isEnabled()`
|
||||
* checks its value first. Override precedence: Settings.Global -> sys.fflag.override.* -> static list.
|
||||
* Set value: `adb shell settings put global settings_enable_monitor_phantom_procs false`
|
||||
* Get value: adb shell settings get global settings_enable_monitor_phantom_procs`
|
||||
* Unset value: `adb shell settings delete global settings_enable_monitor_phantom_procs`
|
||||
*
|
||||
* https://cs.android.com/android/_/android/platform/frameworks/base/+/refs/tags/android-12.0.0_r31:core/java/android/util/FeatureFlagUtils.java;l=113
|
||||
*
|
||||
* The feature flag values can be modified in user builds with settings global list, but since the
|
||||
* developer options feature flags page is not shown and considering that getprop values for features
|
||||
* will be unset by default and settings global list will not be set either and there is no shell API,
|
||||
* it will require an android app process to check if feature is supported on a device and what its
|
||||
* default value is with reflection after bypassing hidden api restrictions since {@link #FEATURE_FLAGS_CLASS}
|
||||
* is annotated as `@hide`.
|
||||
*/
|
||||
public class FeatureFlagUtils {
|
||||
|
||||
public enum FeatureFlagValue {
|
||||
|
||||
/** Unknown like due to exception raised while getting value. */
|
||||
UNKNOWN("<unknown>"),
|
||||
|
||||
/** Flag is unsupported on current android build. */
|
||||
UNSUPPORTED("<unsupported>"),
|
||||
|
||||
/** Flag is enabled. */
|
||||
TRUE("true"),
|
||||
|
||||
/** Flag is not enabled. */
|
||||
FALSE("false");
|
||||
|
||||
private final String name;
|
||||
|
||||
FeatureFlagValue(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static final String FEATURE_FLAGS_CLASS = "android.util.FeatureFlagUtils";
|
||||
|
||||
private static final String LOG_TAG = "FeatureFlagUtils";
|
||||
|
||||
/**
|
||||
* Get all feature flags in their raw form.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Map<String, String> getAllFeatureFlags() {
|
||||
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
|
||||
try {
|
||||
@SuppressLint("PrivateApi") Class<?> clazz = Class.forName(FEATURE_FLAGS_CLASS);
|
||||
Method getAllFeatureFlagsMethod = ReflectionUtils.getDeclaredMethod(clazz, "getAllFeatureFlags");
|
||||
if (getAllFeatureFlagsMethod == null) return null;
|
||||
return (Map<String, String>) ReflectionUtils.invokeMethod(getAllFeatureFlagsMethod, null).value;
|
||||
} catch (Exception e) {
|
||||
// ClassCastException may be thrown
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get all feature flags", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a feature flag exists.
|
||||
*
|
||||
* @return Returns {@code true} if flag exists, otherwise {@code false}. This will be
|
||||
* {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static Boolean featureFlagExists(@NonNull String feature) {
|
||||
Map<String, String> featureFlags = getAllFeatureFlags();
|
||||
if (featureFlags == null) return null;
|
||||
return featureFlags.containsKey(feature);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get {@link FeatureFlagValue} for a feature.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param feature The {@link String} name for feature.
|
||||
* @return Returns {@link FeatureFlagValue}.
|
||||
*/
|
||||
@NonNull
|
||||
public static FeatureFlagValue getFeatureFlagValueString(@NonNull Context context, @NonNull String feature) {
|
||||
Boolean featureFlagExists = featureFlagExists(feature);
|
||||
if (featureFlagExists == null) {
|
||||
Logger.logError(LOG_TAG, "Failed to get feature flags \"" + feature + "\" value");
|
||||
return FeatureFlagValue.UNKNOWN;
|
||||
} else if (!featureFlagExists) {
|
||||
return FeatureFlagValue.UNSUPPORTED;
|
||||
}
|
||||
|
||||
Boolean featureFlagValue = isFeatureEnabled(context, feature);
|
||||
if (featureFlagValue == null) {
|
||||
Logger.logError(LOG_TAG, "Failed to get feature flags \"" + feature + "\" value");
|
||||
return FeatureFlagValue.UNKNOWN;
|
||||
} else {
|
||||
return featureFlagValue ? FeatureFlagValue.TRUE : FeatureFlagValue.FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a feature flag exists.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param feature The {@link String} name for feature.
|
||||
* @return Returns {@code true} if flag exists, otherwise {@code false}. This will be
|
||||
* {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static Boolean isFeatureEnabled(@NonNull Context context, @NonNull String feature) {
|
||||
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
|
||||
try {
|
||||
@SuppressLint("PrivateApi") Class<?> clazz = Class.forName(FEATURE_FLAGS_CLASS);
|
||||
Method isFeatureEnabledMethod = ReflectionUtils.getDeclaredMethod(clazz, "isEnabled", Context.class, String.class);
|
||||
if (isFeatureEnabledMethod == null) {
|
||||
Logger.logError(LOG_TAG, "Failed to check if feature flag \"" + feature + "\" is enabled");
|
||||
return null;
|
||||
}
|
||||
|
||||
return (boolean) ReflectionUtils.invokeMethod(isFeatureEnabledMethod, null, context, feature).value;
|
||||
} catch (Exception e) {
|
||||
// ClassCastException may be thrown
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to check if feature flag \"" + feature + "\" is enabled", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,830 +0,0 @@
|
|||
package com.termux.shared.android;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.app.admin.DevicePolicyManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.UserHandle;
|
||||
import android.os.UserManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.termux.shared.R;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.interact.MessageDialogUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.reflection.ReflectionUtils;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.List;
|
||||
|
||||
public class PackageUtils {
|
||||
|
||||
private static final String LOG_TAG = "PackageUtils";
|
||||
|
||||
/**
|
||||
* Get the {@link Context} for the package name with {@link Context#CONTEXT_RESTRICTED} flags.
|
||||
*
|
||||
* @param context The {@link Context} to use to get the {@link Context} of the {@code packageName}.
|
||||
* @param packageName The package name whose {@link Context} to get.
|
||||
* @return Returns the {@link Context}. This will {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static Context getContextForPackage(@NonNull final Context context, String packageName) {
|
||||
return getContextForPackage(context, packageName, Context.CONTEXT_RESTRICTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Context} for the package name.
|
||||
*
|
||||
* @param context The {@link Context} to use to get the {@link Context} of the {@code packageName}.
|
||||
* @param packageName The package name whose {@link Context} to get.
|
||||
* @param flags The flags for {@link Context} type.
|
||||
* @return Returns the {@link Context}. This will {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static Context getContextForPackage(@NonNull final Context context, String packageName, int flags) {
|
||||
try {
|
||||
return context.createPackageContext(packageName, flags);
|
||||
} catch (Exception e) {
|
||||
Logger.logVerbose(LOG_TAG, "Failed to get \"" + packageName + "\" package context with flags " + flags + ": " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Context} for a package name.
|
||||
*
|
||||
* @param context The {@link Context} to use to get the {@link Context} of the {@code packageName}.
|
||||
* @param packageName The package name whose {@link Context} to get.
|
||||
* @param exitAppOnError If {@code true} and failed to get package context, then a dialog will
|
||||
* be shown which when dismissed will exit the app.
|
||||
* @param helpUrl The help user to add to {@link R.string#error_get_package_context_failed_help_url_message}.
|
||||
* @return Returns the {@link Context}. This will {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static Context getContextForPackageOrExitApp(@NonNull Context context, String packageName,
|
||||
final boolean exitAppOnError, @Nullable String helpUrl) {
|
||||
Context packageContext = getContextForPackage(context, packageName);
|
||||
|
||||
if (packageContext == null && exitAppOnError) {
|
||||
String errorMessage = context.getString(R.string.error_get_package_context_failed_message,
|
||||
packageName);
|
||||
if (!DataUtils.isNullOrEmpty(helpUrl))
|
||||
errorMessage += "\n" + context.getString(R.string.error_get_package_context_failed_help_url_message, helpUrl);
|
||||
Logger.logError(LOG_TAG, errorMessage);
|
||||
MessageDialogUtils.exitAppWithErrorMessage(context,
|
||||
context.getString(R.string.error_get_package_context_failed_title),
|
||||
errorMessage);
|
||||
}
|
||||
|
||||
return packageContext;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the {@link PackageInfo} for the package associated with the {@code context}.
|
||||
*
|
||||
* @param context The {@link Context} for the package.
|
||||
* @return Returns the {@link PackageInfo}. This will be {@code null} if an exception is raised.
|
||||
*/
|
||||
public static PackageInfo getPackageInfoForPackage(@NonNull final Context context) {
|
||||
return getPackageInfoForPackage(context, context.getPackageName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link PackageInfo} for the package associated with the {@code context}.
|
||||
*
|
||||
* @param context The {@link Context} for the package.
|
||||
* @param flags The flags to pass to {@link PackageManager#getPackageInfo(String, int)}.
|
||||
* @return Returns the {@link PackageInfo}. This will be {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static PackageInfo getPackageInfoForPackage(@NonNull final Context context, final int flags) {
|
||||
return getPackageInfoForPackage(context, context.getPackageName(), flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link PackageInfo} for the package associated with the {@code packageName}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param packageName The package name of the package.
|
||||
* @return Returns the {@link PackageInfo}. This will be {@code null} if an exception is raised.
|
||||
*/
|
||||
public static PackageInfo getPackageInfoForPackage(@NonNull final Context context, @NonNull final String packageName) {
|
||||
return getPackageInfoForPackage(context, packageName, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link PackageInfo} for the package associated with the {@code packageName}.
|
||||
*
|
||||
* Also check {@link #isAppInstalled(Context, String, String) if targetting targeting sdk
|
||||
* `30` (android `11`) since {@link PackageManager.NameNotFoundException} may be thrown.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param packageName The package name of the package.
|
||||
* @param flags The flags to pass to {@link PackageManager#getPackageInfo(String, int)}.
|
||||
* @return Returns the {@link PackageInfo}. This will be {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static PackageInfo getPackageInfoForPackage(@NonNull final Context context, @NonNull final String packageName, final int flags) {
|
||||
try {
|
||||
return context.getPackageManager().getPackageInfo(packageName, flags);
|
||||
} catch (final Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the {@link ApplicationInfo} for the {@code packageName}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param packageName The package name of the package.
|
||||
* @return Returns the {@link ApplicationInfo}. This will be {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static ApplicationInfo getApplicationInfoForPackage(@NonNull final Context context, @NonNull final String packageName) {
|
||||
return getApplicationInfoForPackage(context, packageName, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link ApplicationInfo} for the {@code packageName}.
|
||||
*
|
||||
* Also check {@link #isAppInstalled(Context, String, String) if targetting targeting sdk
|
||||
* `30` (android `11`) since {@link PackageManager.NameNotFoundException} may be thrown.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param packageName The package name of the package.
|
||||
* @param flags The flags to pass to {@link PackageManager#getApplicationInfo(String, int)}.
|
||||
* @return Returns the {@link ApplicationInfo}. This will be {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static ApplicationInfo getApplicationInfoForPackage(@NonNull final Context context, @NonNull final String packageName, final int flags) {
|
||||
try {
|
||||
return context.getPackageManager().getApplicationInfo(packageName, flags);
|
||||
} catch (final Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@code privateFlags} {@link Field} of the {@link ApplicationInfo} class.
|
||||
*
|
||||
* @param applicationInfo The {@link ApplicationInfo} for the package.
|
||||
* @return Returns the private flags or {@code null} if an exception was raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static Integer getApplicationInfoPrivateFlagsForPackage(@NonNull final ApplicationInfo applicationInfo) {
|
||||
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
|
||||
try {
|
||||
return (Integer) ReflectionUtils.invokeField(ApplicationInfo.class, "privateFlags", applicationInfo).value;
|
||||
} catch (Exception e) {
|
||||
// ClassCastException may be thrown
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get privateFlags field value for ApplicationInfo class", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@code seInfo} {@link Field} of the {@link ApplicationInfo} class.
|
||||
*
|
||||
* String retrieved from the seinfo tag found in selinux policy. This value can be set through
|
||||
* the mac_permissions.xml policy construct. This value is used for setting an SELinux security
|
||||
* context on the process as well as its data directory.
|
||||
*
|
||||
* https://cs.android.com/android/platform/superproject/+/android-7.1.0_r1:frameworks/base/core/java/android/content/pm/ApplicationInfo.java;l=609
|
||||
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/content/pm/ApplicationInfo.java;l=981
|
||||
* https://cs.android.com/android/platform/superproject/+/android-7.0.0_r1:frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.java;l=282
|
||||
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.java;l=375
|
||||
* https://cs.android.com/android/_/android/platform/frameworks/base/+/be0b8896d1bc385d4c8fb54c21929745935dcbea
|
||||
*
|
||||
* @param applicationInfo The {@link ApplicationInfo} for the package.
|
||||
* @return Returns the selinux info or {@code null} if an exception was raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getApplicationInfoSeInfoForPackage(@NonNull final ApplicationInfo applicationInfo) {
|
||||
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
|
||||
try {
|
||||
return (String) ReflectionUtils.invokeField(ApplicationInfo.class, Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? "seinfo" : "seInfo", applicationInfo).value;
|
||||
} catch (Exception e) {
|
||||
// ClassCastException may be thrown
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get seInfo field value for ApplicationInfo class", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@code seInfoUser} {@link Field} of the {@link ApplicationInfo} class.
|
||||
*
|
||||
* Also check {@link #getApplicationInfoSeInfoForPackage(ApplicationInfo)}.
|
||||
*
|
||||
* @param applicationInfo The {@link ApplicationInfo} for the package.
|
||||
* @return Returns the selinux info user or {@code null} if an exception was raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getApplicationInfoSeInfoUserForPackage(@NonNull final ApplicationInfo applicationInfo) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return null;
|
||||
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
|
||||
try {
|
||||
return (String) ReflectionUtils.invokeField(ApplicationInfo.class, "seInfoUser", applicationInfo).value;
|
||||
} catch (Exception e) {
|
||||
// ClassCastException may be thrown
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get seInfoUser field value for ApplicationInfo class", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@code privateFlags} {@link Field} of the {@link ApplicationInfo} class.
|
||||
*
|
||||
* @param fieldName The name of the field to get.
|
||||
* @return Returns the field value or {@code null} if an exception was raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static Integer getApplicationInfoStaticIntFieldValue(@NonNull String fieldName) {
|
||||
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
|
||||
try {
|
||||
return (Integer) ReflectionUtils.invokeField(ApplicationInfo.class, fieldName, null).value;
|
||||
} catch (Exception e) {
|
||||
// ClassCastException may be thrown
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get \"" + fieldName + "\" field value for ApplicationInfo class", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the app associated with the {@code applicationInfo} has a specific flag set.
|
||||
*
|
||||
* @param flagToCheckName The name of the field for the flag to check.
|
||||
* @param applicationInfo The {@link ApplicationInfo} for the package.
|
||||
* @return Returns {@code true} if app has flag is set, otherwise {@code false}. This will be
|
||||
* {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static Boolean isApplicationInfoPrivateFlagSetForPackage(@NonNull String flagToCheckName, @NonNull final ApplicationInfo applicationInfo) {
|
||||
Integer privateFlags = getApplicationInfoPrivateFlagsForPackage(applicationInfo);
|
||||
if (privateFlags == null) return null;
|
||||
|
||||
Integer flagToCheck = getApplicationInfoStaticIntFieldValue(flagToCheckName);
|
||||
if (flagToCheck == null) return null;
|
||||
|
||||
return ( 0 != ( privateFlags & flagToCheck ) );
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the app name for the package associated with the {@code context}.
|
||||
*
|
||||
* @param context The {@link Context} for the package.
|
||||
* @return Returns the {@code android:name} attribute.
|
||||
*/
|
||||
public static String getAppNameForPackage(@NonNull final Context context) {
|
||||
return getAppNameForPackage(context, context.getApplicationInfo());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the app name for the package associated with the {@code applicationInfo}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param applicationInfo The {@link ApplicationInfo} for the package.
|
||||
* @return Returns the {@code android:name} attribute.
|
||||
*/
|
||||
public static String getAppNameForPackage(@NonNull final Context context, @NonNull final ApplicationInfo applicationInfo) {
|
||||
return applicationInfo.loadLabel(context.getPackageManager()).toString();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the package name for the package associated with the {@code context}.
|
||||
*
|
||||
* @param context The {@link Context} for the package.
|
||||
* @return Returns the package name.
|
||||
*/
|
||||
public static String getPackageNameForPackage(@NonNull final Context context) {
|
||||
return getPackageNameForPackage(context.getApplicationInfo());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the package name for the package associated with the {@code applicationInfo}.
|
||||
*
|
||||
* @param applicationInfo The {@link ApplicationInfo} for the package.
|
||||
* @return Returns the package name.
|
||||
*/
|
||||
public static String getPackageNameForPackage(@NonNull final ApplicationInfo applicationInfo) {
|
||||
return applicationInfo.packageName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the uid for the package associated with the {@code context}.
|
||||
*
|
||||
* @param context The {@link Context} for the package.
|
||||
* @return Returns the uid.
|
||||
*/
|
||||
public static int getUidForPackage(@NonNull final Context context) {
|
||||
return getUidForPackage(context.getApplicationInfo());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the uid for the package associated with the {@code applicationInfo}.
|
||||
*
|
||||
* @param applicationInfo The {@link ApplicationInfo} for the package.
|
||||
* @return Returns the uid.
|
||||
*/
|
||||
public static int getUidForPackage(@NonNull final ApplicationInfo applicationInfo) {
|
||||
return applicationInfo.uid;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the {@code targetSdkVersion} for the package associated with the {@code context}.
|
||||
*
|
||||
* @param context The {@link Context} for the package.
|
||||
* @return Returns the {@code targetSdkVersion}.
|
||||
*/
|
||||
public static int getTargetSDKForPackage(@NonNull final Context context) {
|
||||
return getTargetSDKForPackage(context.getApplicationInfo());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@code targetSdkVersion} for the package associated with the {@code applicationInfo}.
|
||||
*
|
||||
* @param applicationInfo The {@link ApplicationInfo} for the package.
|
||||
* @return Returns the {@code targetSdkVersion}.
|
||||
*/
|
||||
public static int getTargetSDKForPackage(@NonNull final ApplicationInfo applicationInfo) {
|
||||
return applicationInfo.targetSdkVersion;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the base apk path for the package associated with the {@code context}.
|
||||
*
|
||||
* @param context The {@link Context} for the package.
|
||||
* @return Returns the base apk path.
|
||||
*/
|
||||
public static String getBaseAPKPathForPackage(@NonNull final Context context) {
|
||||
return getBaseAPKPathForPackage(context.getApplicationInfo());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base apk path for the package associated with the {@code applicationInfo}.
|
||||
*
|
||||
* @param applicationInfo The {@link ApplicationInfo} for the package.
|
||||
* @return Returns the base apk path.
|
||||
*/
|
||||
public static String getBaseAPKPathForPackage(@NonNull final ApplicationInfo applicationInfo) {
|
||||
return applicationInfo.publicSourceDir;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Check if the app associated with the {@code context} has {@link ApplicationInfo#FLAG_DEBUGGABLE}
|
||||
* set.
|
||||
*
|
||||
* @param context The {@link Context} for the package.
|
||||
* @return Returns {@code true} if app is debuggable, otherwise {@code false}.
|
||||
*/
|
||||
public static boolean isAppForPackageADebuggableBuild(@NonNull final Context context) {
|
||||
return isAppForPackageADebuggableBuild(context.getApplicationInfo());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the app associated with the {@code applicationInfo} has {@link ApplicationInfo#FLAG_DEBUGGABLE}
|
||||
* set.
|
||||
*
|
||||
* @param applicationInfo The {@link ApplicationInfo} for the package.
|
||||
* @return Returns {@code true} if app is debuggable, otherwise {@code false}.
|
||||
*/
|
||||
public static boolean isAppForPackageADebuggableBuild(@NonNull final ApplicationInfo applicationInfo) {
|
||||
return ( 0 != ( applicationInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE ) );
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Check if the app associated with the {@code context} has {@link ApplicationInfo#FLAG_EXTERNAL_STORAGE}
|
||||
* set.
|
||||
*
|
||||
* @param context The {@link Context} for the package.
|
||||
* @return Returns {@code true} if app is installed on external storage, otherwise {@code false}.
|
||||
*/
|
||||
public static boolean isAppInstalledOnExternalStorage(@NonNull final Context context) {
|
||||
return isAppInstalledOnExternalStorage(context.getApplicationInfo());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the app associated with the {@code applicationInfo} has {@link ApplicationInfo#FLAG_EXTERNAL_STORAGE}
|
||||
* set.
|
||||
*
|
||||
* @param applicationInfo The {@link ApplicationInfo} for the package.
|
||||
* @return Returns {@code true} if app is installed on external storage, otherwise {@code false}.
|
||||
*/
|
||||
public static boolean isAppInstalledOnExternalStorage(@NonNull final ApplicationInfo applicationInfo) {
|
||||
return ( 0 != ( applicationInfo.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE ) );
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Check if the app associated with the {@code context} has
|
||||
* ApplicationInfo.PRIVATE_FLAG_REQUEST_LEGACY_EXTERNAL_STORAGE (requestLegacyExternalStorage)
|
||||
* set to {@code true} in app manifest.
|
||||
*
|
||||
* @param context The {@link Context} for the package.
|
||||
* @return Returns {@code true} if app has requested legacy external storage, otherwise
|
||||
* {@code false}. This will be {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static Boolean hasRequestedLegacyExternalStorage(@NonNull final Context context) {
|
||||
return hasRequestedLegacyExternalStorage(context.getApplicationInfo());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the app associated with the {@code applicationInfo} has
|
||||
* ApplicationInfo.PRIVATE_FLAG_REQUEST_LEGACY_EXTERNAL_STORAGE (requestLegacyExternalStorage)
|
||||
* set to {@code true} in app manifest.
|
||||
*
|
||||
* @param applicationInfo The {@link ApplicationInfo} for the package.
|
||||
* @return Returns {@code true} if app has requested legacy external storage, otherwise
|
||||
* {@code false}. This will be {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static Boolean hasRequestedLegacyExternalStorage(@NonNull final ApplicationInfo applicationInfo) {
|
||||
return isApplicationInfoPrivateFlagSetForPackage("PRIVATE_FLAG_REQUEST_LEGACY_EXTERNAL_STORAGE", applicationInfo);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the {@code versionCode} for the package associated with the {@code context}.
|
||||
*
|
||||
* @param context The {@link Context} for the package.
|
||||
* @return Returns the {@code versionCode}. This will be {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static Integer getVersionCodeForPackage(@NonNull final Context context) {
|
||||
return getVersionCodeForPackage(context, context.getPackageName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@code versionCode} for the {@code packageName}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param packageName The package name of the package.
|
||||
* @return Returns the {@code versionCode}. This will be {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static Integer getVersionCodeForPackage(@NonNull final Context context, @NonNull final String packageName) {
|
||||
return getVersionCodeForPackage(getPackageInfoForPackage(context, packageName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@code versionCode} for the {@code packageName}.
|
||||
*
|
||||
* @param packageInfo The {@link PackageInfo} for the package.
|
||||
* @return Returns the {@code versionCode}. This will be {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static Integer getVersionCodeForPackage(@Nullable final PackageInfo packageInfo) {
|
||||
return packageInfo != null ? packageInfo.versionCode : null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the {@code versionName} for the package associated with the {@code context}.
|
||||
*
|
||||
* @param context The {@link Context} for the package.
|
||||
* @return Returns the {@code versionName}. This will be {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getVersionNameForPackage(@NonNull final Context context) {
|
||||
return getVersionNameForPackage(context, context.getPackageName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@code versionName} for the {@code packageName}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param packageName The package name of the package.
|
||||
* @return Returns the {@code versionName}. This will be {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getVersionNameForPackage(@NonNull final Context context, @NonNull final String packageName) {
|
||||
return getVersionNameForPackage(getPackageInfoForPackage(context, packageName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@code versionName} for the {@code packageName}.
|
||||
*
|
||||
* @param packageInfo The {@link PackageInfo} for the package.
|
||||
* @return Returns the {@code versionName}. This will be {@code null} if an {@code packageInfo}
|
||||
* is {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getVersionNameForPackage(@Nullable final PackageInfo packageInfo) {
|
||||
return packageInfo != null ? packageInfo.versionName : null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the {@code SHA-256 digest} of signing certificate for the package associated with the {@code context}.
|
||||
*
|
||||
* @param context The {@link Context} for the package.
|
||||
* @return Returns the {@code SHA-256 digest}. This will be {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getSigningCertificateSHA256DigestForPackage(@NonNull final Context context) {
|
||||
return getSigningCertificateSHA256DigestForPackage(context, context.getPackageName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@code SHA-256 digest} of signing certificate for the {@code packageName}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param packageName The package name of the package.
|
||||
* @return Returns the {@code SHA-256 digest}. This will be {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getSigningCertificateSHA256DigestForPackage(@NonNull final Context context, @NonNull final String packageName) {
|
||||
try {
|
||||
/*
|
||||
* Todo: We may need AndroidManifest queries entries if package is installed but with a different signature on android 11
|
||||
* https://developer.android.com/training/package-visibility
|
||||
* Need a device that allows (manual) installation of apk with mismatched signature of
|
||||
* sharedUserId apps to test. Currently, if its done, PackageManager just doesn't load
|
||||
* the package and removes its apk automatically if its installed as a user app instead of system app
|
||||
* W/PackageManager: Failed to parse /path/to/com.termux.tasker.apk: Signature mismatch for shared user: SharedUserSetting{xxxxxxx com.termux/10xxx}
|
||||
*/
|
||||
PackageInfo packageInfo = getPackageInfoForPackage(context, packageName, PackageManager.GET_SIGNATURES);
|
||||
if (packageInfo == null) return null;
|
||||
return DataUtils.bytesToHex(MessageDigest.getInstance("SHA-256").digest(packageInfo.signatures[0].toByteArray()));
|
||||
} catch (final Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the serial number for the user for the package associated with the {@code context}.
|
||||
*
|
||||
* @param context The {@link Context} for the package.
|
||||
* @return Returns the serial number. This will be {@code null} if failed to get it.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
@Nullable
|
||||
public static Long getUserIdForPackage(@NonNull Context context) {
|
||||
UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
|
||||
if (userManager == null) return null;
|
||||
return userManager.getSerialNumberForUser(UserHandle.getUserHandleForUid(getUidForPackage(context)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user is the primary user. This is done by checking if the the serial
|
||||
* number for the current user equals 0.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @return Returns {@code true} if the current user is the primary user, otherwise [@code false}.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public static boolean isCurrentUserThePrimaryUser(@NonNull Context context) {
|
||||
Long userId = getUserIdForPackage(context);
|
||||
return userId != null && userId == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the profile owner package name for the current user.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @return Returns the profile owner package name. This will be {@code null} if failed to get it
|
||||
* or no profile owner for the current user.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getProfileOwnerPackageNameForUser(@NonNull Context context) {
|
||||
DevicePolicyManager devicePolicyManager = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
|
||||
if (devicePolicyManager == null) return null;
|
||||
List<ComponentName> activeAdmins = devicePolicyManager.getActiveAdmins();
|
||||
if (activeAdmins != null){
|
||||
for (ComponentName admin:activeAdmins){
|
||||
String packageName = admin.getPackageName();
|
||||
if(devicePolicyManager.isProfileOwnerApp(packageName))
|
||||
return packageName;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the process id of the main app process of a package. This will work for sharedUserId. Note
|
||||
* that some apps have multiple processes for the app like with `android:process=":background"`
|
||||
* attribute in AndroidManifest.xml.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param packageName The package name of the process.
|
||||
* @return Returns the process if found and running, otherwise {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getPackagePID(final Context context, String packageName) {
|
||||
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
if (activityManager != null) {
|
||||
List<ActivityManager.RunningAppProcessInfo> processInfos = activityManager.getRunningAppProcesses();
|
||||
if (processInfos != null) {
|
||||
ActivityManager.RunningAppProcessInfo processInfo;
|
||||
for (int i = 0; i < processInfos.size(); i++) {
|
||||
processInfo = processInfos.get(i);
|
||||
if (processInfo.processName.equals(packageName))
|
||||
return String.valueOf(processInfo.pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Check if app is installed and enabled. This can be used by external apps that don't
|
||||
* share `sharedUserId` with the an app.
|
||||
*
|
||||
* If your third-party app is targeting sdk `30` (android `11`), then it needs to add package
|
||||
* name to the `queries` element or request `QUERY_ALL_PACKAGES` permission in its
|
||||
* `AndroidManifest.xml`. Otherwise it will get `PackageSetting{...... package_name/......} BLOCKED`
|
||||
* errors in `logcat` and {@link PackageManager.NameNotFoundException} may be thrown.
|
||||
* `RUN_COMMAND` intent won't work either.
|
||||
* Check [package-visibility](https://developer.android.com/training/basics/intents/package-visibility#package-name),
|
||||
* `QUERY_ALL_PACKAGES` [googleplay policy](https://support.google.com/googleplay/android-developer/answer/10158779
|
||||
* and this [article](https://medium.com/androiddevelopers/working-with-package-visibility-dc252829de2d) for more info.
|
||||
*
|
||||
* {@code
|
||||
* <manifest
|
||||
* <queries>
|
||||
* <package android:name="com.termux" />
|
||||
* </queries>
|
||||
*
|
||||
* <application
|
||||
* ....
|
||||
* </application>
|
||||
* </manifest>
|
||||
* }
|
||||
*
|
||||
* @param context The context for operations.
|
||||
* @param appName The name of the app.
|
||||
* @param packageName The package name of the package.
|
||||
* @return Returns {@code errmsg} if {@code packageName} is not installed or disabled, otherwise {@code null}.
|
||||
*/
|
||||
public static String isAppInstalled(@NonNull final Context context, String appName, String packageName) {
|
||||
String errmsg = null;
|
||||
|
||||
ApplicationInfo applicationInfo = getApplicationInfoForPackage(context, packageName);
|
||||
boolean isAppEnabled = (applicationInfo != null && applicationInfo.enabled);
|
||||
|
||||
// If app is not installed or is disabled
|
||||
if (!isAppEnabled)
|
||||
errmsg = context.getString(R.string.error_app_not_installed_or_disabled_warning, appName, packageName);
|
||||
|
||||
return errmsg;
|
||||
}
|
||||
|
||||
|
||||
/** Wrapper for {@link #setComponentState(Context, String, String, boolean, String, boolean, boolean)} with
|
||||
* {@code alwaysShowToast} {@code true}. */
|
||||
public static String setComponentState(@NonNull final Context context, @NonNull String packageName,
|
||||
@NonNull String className, boolean newState, String toastString,
|
||||
boolean showErrorMessage) {
|
||||
return setComponentState(context, packageName, className, newState, toastString, showErrorMessage, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable a {@link ComponentName} with a call to
|
||||
* {@link PackageManager#setComponentEnabledSetting(ComponentName, int, int)}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param packageName The package name of the component.
|
||||
* @param className The {@link Class} name of the component.
|
||||
* @param newState If component should be enabled or disabled.
|
||||
* @param toastString If this is not {@code null} or empty, then a toast before setting state.
|
||||
* @param showErrorMessage If an error message toast should be shown.
|
||||
* @param alwaysShowToast If toast should always be shown even if current state matches new state.
|
||||
* @return Returns the errmsg if failed to set state, otherwise {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
public static String setComponentState(@NonNull final Context context, @NonNull String packageName,
|
||||
@NonNull String className, boolean newState, String toastString,
|
||||
boolean alwaysShowToast, boolean showErrorMessage) {
|
||||
try {
|
||||
PackageManager packageManager = context.getPackageManager();
|
||||
if (packageManager != null) {
|
||||
if (toastString != null && alwaysShowToast) {
|
||||
Logger.showToast(context, toastString, true);
|
||||
toastString = null;
|
||||
}
|
||||
|
||||
Boolean currentlyDisabled = PackageUtils.isComponentDisabled(context, packageName, className, false);
|
||||
if (currentlyDisabled == null)
|
||||
throw new UnsupportedOperationException("Failed to find if component currently disabled");
|
||||
|
||||
Boolean setState = null;
|
||||
if (newState && currentlyDisabled)
|
||||
setState = true;
|
||||
else if (!newState && !currentlyDisabled)
|
||||
setState = false;
|
||||
|
||||
if (setState == null) return null;
|
||||
|
||||
if (toastString != null) Logger.showToast(context, toastString, true);
|
||||
ComponentName componentName = new ComponentName(packageName, className);
|
||||
packageManager.setComponentEnabledSetting(componentName,
|
||||
setState ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
|
||||
PackageManager.DONT_KILL_APP);
|
||||
}
|
||||
return null;
|
||||
} catch (final Exception e) {
|
||||
String errmsg = context.getString(
|
||||
newState ? R.string.error_enable_component_failed : R.string.error_disable_component_failed,
|
||||
packageName, className) + ": " + e.getMessage();
|
||||
if (showErrorMessage)
|
||||
Logger.showToast(context, errmsg, true);
|
||||
return errmsg;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if state of a {@link ComponentName} is {@link PackageManager#COMPONENT_ENABLED_STATE_DISABLED}
|
||||
* with a call to {@link PackageManager#getComponentEnabledSetting(ComponentName)}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param packageName The package name of the component.
|
||||
* @param className The {@link Class} name of the component.
|
||||
* @param logErrorMessage If an error message should be logged.
|
||||
* @return Returns {@code true} if disabled, {@code false} if not and {@code null} if failed to
|
||||
* get the state.
|
||||
*/
|
||||
public static Boolean isComponentDisabled(@NonNull final Context context, @NonNull String packageName,
|
||||
@NonNull String className, boolean logErrorMessage) {
|
||||
try {
|
||||
PackageManager packageManager = context.getPackageManager();
|
||||
if (packageManager != null) {
|
||||
ComponentName componentName = new ComponentName(packageName, className);
|
||||
// Will throw IllegalArgumentException: Unknown component: ComponentInfo{} if app
|
||||
// for context is not installed or component does not exist.
|
||||
return packageManager.getComponentEnabledSetting(componentName) == PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
if (logErrorMessage)
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, context.getString(R.string.error_get_component_state_failed, packageName, className), e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an {@link android.app.Activity} {@link ComponentName} can be called by calling
|
||||
* {@link PackageManager#queryIntentActivities(Intent, int)}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param packageName The package name of the component.
|
||||
* @param className The {@link Class} name of the component.
|
||||
* @param flags The flags to filter results.
|
||||
* @return Returns {@code true} if it exists, otherwise {@code false}.
|
||||
*/
|
||||
public static boolean doesActivityComponentExist(@NonNull final Context context, @NonNull String packageName,
|
||||
@NonNull String className, int flags) {
|
||||
try {
|
||||
PackageManager packageManager = context.getPackageManager();
|
||||
if (packageManager != null) {
|
||||
Intent intent = new Intent();
|
||||
intent.setClassName(packageName, className);
|
||||
return packageManager.queryIntentActivities(intent, flags).size() > 0;
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,573 +0,0 @@
|
|||
package com.termux.shared.android;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.os.PowerManager;
|
||||
import android.provider.Settings;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.termux.shared.R;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.errors.Error;
|
||||
import com.termux.shared.errors.FunctionErrno;
|
||||
import com.termux.shared.activity.ActivityUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class PermissionUtils {
|
||||
|
||||
public static final int REQUEST_GRANT_STORAGE_PERMISSION = 1000;
|
||||
|
||||
public static final int REQUEST_DISABLE_BATTERY_OPTIMIZATIONS = 2000;
|
||||
public static final int REQUEST_GRANT_DISPLAY_OVER_OTHER_APPS_PERMISSION = 2001;
|
||||
|
||||
private static final String LOG_TAG = "PermissionUtils";
|
||||
|
||||
|
||||
/**
|
||||
* Check if app has been granted the required permission.
|
||||
*
|
||||
* @param context The context for operations.
|
||||
* @param permission The {@link String} name for permission to check.
|
||||
* @return Returns {@code true} if permission is granted, otherwise {@code false}.
|
||||
*/
|
||||
public static boolean checkPermission(@NonNull Context context, @NonNull String permission) {
|
||||
return checkPermissions(context, new String[]{permission});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if app has been granted the required permissions.
|
||||
*
|
||||
* @param context The context for operations.
|
||||
* @param permissions The {@link String[]} names for permissions to check.
|
||||
* @return Returns {@code true} if permissions are granted, otherwise {@code false}.
|
||||
*/
|
||||
public static boolean checkPermissions(@NonNull Context context, @NonNull String[] permissions) {
|
||||
// checkSelfPermission may return true for permissions not even requested
|
||||
List<String> permissionsNotRequested = getPermissionsNotRequested(context, permissions);
|
||||
if (permissionsNotRequested.size() > 0) {
|
||||
Logger.logError(LOG_TAG,
|
||||
context.getString(R.string.error_attempted_to_check_for_permissions_not_requested,
|
||||
Joiner.on(", ").join(permissionsNotRequested)));
|
||||
return false;
|
||||
}
|
||||
|
||||
int result;
|
||||
for (String permission : permissions) {
|
||||
result = ContextCompat.checkSelfPermission(context, permission);
|
||||
if (result != PackageManager.PERMISSION_GRANTED) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Request user to grant required permissions to the app.
|
||||
*
|
||||
* @param context The context for operations. It must be an instance of {@link Activity} or
|
||||
* {@link AppCompatActivity}.
|
||||
* @param permission The {@link String} name for permission to request.
|
||||
* @param requestCode The request code to use while asking for permission. It must be `>=0` or
|
||||
* will fail silently and will log an exception.
|
||||
* @return Returns {@code true} if requesting the permission was successful, otherwise {@code false}.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public static boolean requestPermission(@NonNull Context context, @NonNull String permission,
|
||||
int requestCode) {
|
||||
return requestPermissions(context, new String[]{permission}, requestCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request user to grant required permissions to the app.
|
||||
*
|
||||
* On sdk 30 (android 11), Activity.onRequestPermissionsResult() will pass
|
||||
* {@link PackageManager#PERMISSION_DENIED} (-1) without asking the user for the permission
|
||||
* if user previously denied the permission prompt. On sdk 29 (android 10),
|
||||
* Activity.onRequestPermissionsResult() will pass {@link PackageManager#PERMISSION_DENIED} (-1)
|
||||
* without asking the user for the permission if user previously selected "Deny & don't ask again"
|
||||
* option in prompt. The user will have to manually enable permission in app info in Android
|
||||
* settings. If user grants and then denies in settings, then next time prompt will shown.
|
||||
*
|
||||
* @param context The context for operations. It must be an instance of {@link Activity} or
|
||||
* {@link AppCompatActivity}.
|
||||
* @param permissions The {@link String[]} names for permissions to request.
|
||||
* @param requestCode The request code to use while asking for permissions. It must be `>=0` or
|
||||
* will fail silently and will log an exception.
|
||||
* @return Returns {@code true} if requesting the permissions was successful, otherwise {@code false}.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public static boolean requestPermissions(@NonNull Context context, @NonNull String[] permissions,
|
||||
int requestCode) {
|
||||
List<String> permissionsNotRequested = getPermissionsNotRequested(context, permissions);
|
||||
if (permissionsNotRequested.size() > 0) {
|
||||
Logger.logErrorAndShowToast(context, LOG_TAG,
|
||||
context.getString(R.string.error_attempted_to_ask_for_permissions_not_requested,
|
||||
Joiner.on(", ").join(permissionsNotRequested)));
|
||||
return false;
|
||||
}
|
||||
|
||||
for (String permission : permissions) {
|
||||
int result = ContextCompat.checkSelfPermission(context, permission);
|
||||
// If at least one permission not granted
|
||||
if (result != PackageManager.PERMISSION_GRANTED) {
|
||||
Logger.logInfo(LOG_TAG, "Requesting Permissions: " + Arrays.toString(permissions));
|
||||
|
||||
try {
|
||||
if (context instanceof AppCompatActivity)
|
||||
((AppCompatActivity) context).requestPermissions(permissions, requestCode);
|
||||
else if (context instanceof Activity)
|
||||
((Activity) context).requestPermissions(permissions, requestCode);
|
||||
else {
|
||||
Error.logErrorAndShowToast(context, LOG_TAG,
|
||||
FunctionErrno.ERRNO_PARAMETER_NOT_INSTANCE_OF.getError("context", "requestPermissions", "Activity or AppCompatActivity"));
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
String errmsg = context.getString(R.string.error_failed_to_request_permissions, requestCode, Arrays.toString(permissions));
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, errmsg, e);
|
||||
Logger.showToast(context, errmsg + "\n" + e.getMessage(), true);
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Check if app has requested the required permission in the manifest.
|
||||
*
|
||||
* @param context The context for operations.
|
||||
* @param permission The {@link String} name for permission to check.
|
||||
* @return Returns {@code true} if permission has been requested, otherwise {@code false}.
|
||||
*/
|
||||
public static boolean isPermissionRequested(@NonNull Context context, @NonNull String permission) {
|
||||
return getPermissionsNotRequested(context, new String[]{permission}).size() == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if app has requested the required permissions or not in the manifest.
|
||||
*
|
||||
* @param context The context for operations.
|
||||
* @param permissions The {@link String[]} names for permissions to check.
|
||||
* @return Returns {@link List<String>} of permissions that have not been requested. It will have
|
||||
* size 0 if all permissions have been requested.
|
||||
*/
|
||||
@NonNull
|
||||
public static List<String> getPermissionsNotRequested(@NonNull Context context, @NonNull String[] permissions) {
|
||||
List<String> permissionsNotRequested = new ArrayList<>();
|
||||
Collections.addAll(permissionsNotRequested, permissions);
|
||||
|
||||
PackageInfo packageInfo = PackageUtils.getPackageInfoForPackage(context, PackageManager.GET_PERMISSIONS);
|
||||
if (packageInfo == null) {
|
||||
return permissionsNotRequested;
|
||||
}
|
||||
|
||||
// If no permissions are requested, then nothing to check
|
||||
if (packageInfo.requestedPermissions == null || packageInfo.requestedPermissions.length == 0)
|
||||
return permissionsNotRequested;
|
||||
|
||||
List<String> requestedPermissionsList = Arrays.asList(packageInfo.requestedPermissions);
|
||||
for (String permission : permissions) {
|
||||
if (requestedPermissionsList.contains(permission)) {
|
||||
permissionsNotRequested.remove(permission);
|
||||
}
|
||||
}
|
||||
|
||||
return permissionsNotRequested;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/** If path is under primary external storage directory and storage permission is missing,
|
||||
* then legacy or manage external storage permission will be requested from the user via a call
|
||||
* to {@link #checkAndRequestLegacyOrManageExternalStoragePermission(Context, int, boolean)}.
|
||||
*
|
||||
* @param context The context for operations.
|
||||
* @param filePath The path to check.
|
||||
* @param requestCode The request code to use while asking for permission.
|
||||
* @param showErrorMessage If an error message toast should be shown if permission is not granted.
|
||||
* @return Returns {@code true} if permission is granted, otherwise {@code false}.
|
||||
*/
|
||||
@SuppressLint("SdCardPath")
|
||||
public static boolean checkAndRequestLegacyOrManageExternalStoragePermissionIfPathOnPrimaryExternalStorage(
|
||||
@NonNull Context context, String filePath, int requestCode, boolean showErrorMessage) {
|
||||
// If path is under primary external storage directory, then check for missing permissions.
|
||||
if (!FileUtils.isPathInDirPaths(filePath,
|
||||
Arrays.asList(Environment.getExternalStorageDirectory().getAbsolutePath(), "/sdcard"), true))
|
||||
return true;
|
||||
|
||||
return checkAndRequestLegacyOrManageExternalStoragePermission(context, requestCode, showErrorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if legacy or manage external storage permissions has been granted. If
|
||||
* {@link #isLegacyExternalStoragePossible(Context)} returns {@code true}, them it will be
|
||||
* checked if app has has been granted {@link Manifest.permission#READ_EXTERNAL_STORAGE} and
|
||||
* {@link Manifest.permission#WRITE_EXTERNAL_STORAGE} permissions, otherwise it will be checked
|
||||
* if app has been granted the {@link Manifest.permission#MANAGE_EXTERNAL_STORAGE} permission.
|
||||
*
|
||||
* If storage permission is missing, it will be requested from the user if {@code context} is an
|
||||
* instance of {@link Activity} or {@link AppCompatActivity} and {@code requestCode}
|
||||
* is `>=0` and the function will automatically return. The caller should register for
|
||||
* Activity.onActivityResult() and Activity.onRequestPermissionsResult() and call this function
|
||||
* again but set {@code requestCode} to `-1` to check if permission was granted or not.
|
||||
*
|
||||
* Caller must add following to AndroidManifest.xml of the app, otherwise errors will be thrown.
|
||||
* {@code
|
||||
* <manifest
|
||||
* <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" />
|
||||
*
|
||||
* <application
|
||||
* android:requestLegacyExternalStorage="true"
|
||||
* ....
|
||||
* </application>
|
||||
* </manifest>
|
||||
*}
|
||||
* @param context The context for operations.
|
||||
* @param requestCode The request code to use while asking for permission.
|
||||
* @param showErrorMessage If an error message toast should be shown if permission is not granted.
|
||||
* @return Returns {@code true} if permission is granted, otherwise {@code false}.
|
||||
*/
|
||||
public static boolean checkAndRequestLegacyOrManageExternalStoragePermission(@NonNull Context context,
|
||||
int requestCode,
|
||||
boolean showErrorMessage) {
|
||||
String errmsg;
|
||||
boolean requestLegacyStoragePermission = isLegacyExternalStoragePossible(context);
|
||||
boolean checkIfHasRequestedLegacyExternalStorage = checkIfHasRequestedLegacyExternalStorage(context);
|
||||
|
||||
if (requestLegacyStoragePermission && checkIfHasRequestedLegacyExternalStorage) {
|
||||
// Check if requestLegacyExternalStorage is set to true in app manifest
|
||||
if (!hasRequestedLegacyExternalStorage(context, showErrorMessage))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (checkStoragePermission(context, requestLegacyStoragePermission)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
errmsg = context.getString(R.string.msg_storage_permission_not_granted);
|
||||
Logger.logError(LOG_TAG, errmsg);
|
||||
if (showErrorMessage)
|
||||
Logger.showToast(context, errmsg, false);
|
||||
|
||||
if (requestCode < 0 || Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
|
||||
return false;
|
||||
|
||||
if (requestLegacyStoragePermission || Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
requestLegacyStorageExternalPermission(context, requestCode);
|
||||
} else {
|
||||
requestManageStorageExternalPermission(context, requestCode);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if app has been granted storage permission.
|
||||
*
|
||||
* @param context The context for operations.
|
||||
* @param checkLegacyStoragePermission If set to {@code true}, then it will be checked if app
|
||||
* has been granted {@link Manifest.permission#READ_EXTERNAL_STORAGE}
|
||||
* and {@link Manifest.permission#WRITE_EXTERNAL_STORAGE}
|
||||
* permissions, otherwise it will be checked if app has been
|
||||
* granted the {@link Manifest.permission#MANAGE_EXTERNAL_STORAGE}
|
||||
* permission.
|
||||
* @return Returns {@code true} if permission is granted, otherwise {@code false}.
|
||||
*/
|
||||
public static boolean checkStoragePermission(@NonNull Context context, boolean checkLegacyStoragePermission) {
|
||||
if (checkLegacyStoragePermission || Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
return checkPermissions(context,
|
||||
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE});
|
||||
} else {
|
||||
return Environment.isExternalStorageManager();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request user to grant {@link Manifest.permission#READ_EXTERNAL_STORAGE} and
|
||||
* {@link Manifest.permission#WRITE_EXTERNAL_STORAGE} permissions to the app.
|
||||
*
|
||||
* @param context The context for operations. It must be an instance of {@link Activity} or
|
||||
* {@link AppCompatActivity}.
|
||||
* @param requestCode The request code to use while asking for permission. It must be `>=0` or
|
||||
* will fail silently and will log an exception.
|
||||
* @return Returns {@code true} if requesting the permission was successful, otherwise {@code false}.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public static boolean requestLegacyStorageExternalPermission(@NonNull Context context, int requestCode) {
|
||||
Logger.logInfo(LOG_TAG, "Requesting legacy external storage permission");
|
||||
return requestPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE, requestCode);
|
||||
}
|
||||
|
||||
/** Wrapper for {@link #requestManageStorageExternalPermission(Context, int)}. */
|
||||
@RequiresApi(api = Build.VERSION_CODES.R)
|
||||
public static Error requestManageStorageExternalPermission(@NonNull Context context) {
|
||||
return requestManageStorageExternalPermission(context, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request user to grant {@link Manifest.permission#MANAGE_EXTERNAL_STORAGE} permission to the app.
|
||||
*
|
||||
* @param context The context for operations, like an {@link Activity} or {@link Service} context.
|
||||
* It must be an instance of {@link Activity} or {@link AppCompatActivity} if
|
||||
* result is required via the Activity#onActivityResult() callback and
|
||||
* {@code requestCode} is `>=0`.
|
||||
* @param requestCode The request code to use while asking for permission. It must be `>=0` if
|
||||
* result it required.
|
||||
* @return Returns the {@code error} if requesting the permission was not successful, otherwise {@code null}.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.R)
|
||||
public static Error requestManageStorageExternalPermission(@NonNull Context context, int requestCode) {
|
||||
Logger.logInfo(LOG_TAG, "Requesting manage external storage permission");
|
||||
|
||||
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
|
||||
intent.addCategory("android.intent.category.DEFAULT");
|
||||
intent.setData(Uri.parse("package:" + context.getPackageName()));
|
||||
|
||||
// Flag must not be passed for activity contexts, otherwise onActivityResult() will not be called with permission grant result.
|
||||
// Flag must be passed for non-activity contexts like services, otherwise "Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag" exception will be raised.
|
||||
if (!(context instanceof Activity))
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
Error error;
|
||||
if (requestCode >=0)
|
||||
error = ActivityUtils.startActivityForResult(context, requestCode, intent, true, false);
|
||||
else
|
||||
error = ActivityUtils.startActivity(context, intent, true, false);
|
||||
|
||||
// Use fallback if matching Activity did not exist for ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION.
|
||||
if (error != null) {
|
||||
intent = new Intent();
|
||||
intent.setAction(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
|
||||
if (requestCode >=0)
|
||||
return ActivityUtils.startActivityForResult(context, requestCode, intent);
|
||||
else
|
||||
return ActivityUtils.startActivity(context, intent);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* If app is targeting targetSdkVersion 30 (android 11) and running on sdk 30 (android 11) or
|
||||
* higher, then {@link android.R.attr#requestLegacyExternalStorage} attribute is ignored.
|
||||
* https://developer.android.com/training/data-storage/use-cases#opt-out-scoped-storage
|
||||
*/
|
||||
public static boolean isLegacyExternalStoragePossible(@NonNull Context context) {
|
||||
return !(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
|
||||
PackageUtils.getTargetSDKForPackage(context) >= Build.VERSION_CODES.R);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether it should be checked if app has set
|
||||
* {@link android.R.attr#requestLegacyExternalStorage} attribute to {@code true}, if storage
|
||||
* permissions are to be requested based on if {@link #isLegacyExternalStoragePossible(Context)}
|
||||
* return {@code true}.
|
||||
*
|
||||
* If app is targeting targetSdkVersion 30 (android 11), then legacy storage can only be
|
||||
* requested if running on sdk 29 (android 10).
|
||||
* If app is targeting targetSdkVersion 29 (android 10), then legacy storage can only be
|
||||
* requested if running on sdk 29 (android 10) and higher.
|
||||
*/
|
||||
public static boolean checkIfHasRequestedLegacyExternalStorage(@NonNull Context context) {
|
||||
int targetSdkVersion = PackageUtils.getTargetSDKForPackage(context);
|
||||
|
||||
if (targetSdkVersion >= Build.VERSION_CODES.R) {
|
||||
return Build.VERSION.SDK_INT == Build.VERSION_CODES.Q;
|
||||
} else if (targetSdkVersion == Build.VERSION_CODES.Q) {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call to {@link Environment#isExternalStorageLegacy()} will not return the actual value defined
|
||||
* in app manifest for {@link android.R.attr#requestLegacyExternalStorage} attribute,
|
||||
* since an app may inherit its legacy state based on when it was first installed, target sdk and
|
||||
* other factors. To provide consistent experience for all users regardless of current legacy
|
||||
* state on a specific device, we directly use the value defined in app` manifest.
|
||||
*/
|
||||
public static boolean hasRequestedLegacyExternalStorage(@NonNull Context context,
|
||||
boolean showErrorMessage) {
|
||||
String errmsg;
|
||||
Boolean hasRequestedLegacyExternalStorage = PackageUtils.hasRequestedLegacyExternalStorage(context);
|
||||
if (hasRequestedLegacyExternalStorage != null && !hasRequestedLegacyExternalStorage) {
|
||||
errmsg = context.getString(R.string.error_has_not_requested_legacy_external_storage,
|
||||
context.getPackageName(), PackageUtils.getTargetSDKForPackage(context), Build.VERSION.SDK_INT);
|
||||
Logger.logError(LOG_TAG, errmsg);
|
||||
if (showErrorMessage)
|
||||
Logger.showToast(context, errmsg, true);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Check if {@link Manifest.permission#SYSTEM_ALERT_WINDOW} permission has been granted.
|
||||
*
|
||||
* @param context The context for operations.
|
||||
* @return Returns {@code true} if permission is granted, otherwise {@code false}.
|
||||
*/
|
||||
public static boolean checkDisplayOverOtherAppsPermission(@NonNull Context context) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M)
|
||||
return Settings.canDrawOverlays(context);
|
||||
else
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Wrapper for {@link #requestDisplayOverOtherAppsPermission(Context, int)}. */
|
||||
public static Error requestDisplayOverOtherAppsPermission(@NonNull Context context) {
|
||||
return requestDisplayOverOtherAppsPermission(context, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request user to grant {@link Manifest.permission#SYSTEM_ALERT_WINDOW} permission to the app.
|
||||
*
|
||||
* @param context The context for operations, like an {@link Activity} or {@link Service} context.
|
||||
* It must be an instance of {@link Activity} or {@link AppCompatActivity} if
|
||||
* result is required via the Activity#onActivityResult() callback and
|
||||
* {@code requestCode} is `>=0`.
|
||||
* @param requestCode The request code to use while asking for permission. It must be `>=0` if
|
||||
* result it required.
|
||||
* @return Returns the {@code error} if requesting the permission was not successful, otherwise {@code null}.
|
||||
*/
|
||||
public static Error requestDisplayOverOtherAppsPermission(@NonNull Context context, int requestCode) {
|
||||
Logger.logInfo(LOG_TAG, "Requesting display over apps permission");
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
|
||||
return null;
|
||||
|
||||
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
|
||||
intent.setData(Uri.parse("package:" + context.getPackageName()));
|
||||
|
||||
// Flag must not be passed for activity contexts, otherwise onActivityResult() will not be called with permission grant result.
|
||||
// Flag must be passed for non-activity contexts like services, otherwise "Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag" exception will be raised.
|
||||
if (!(context instanceof Activity))
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
if (requestCode >=0)
|
||||
return ActivityUtils.startActivityForResult(context, requestCode, intent);
|
||||
else
|
||||
return ActivityUtils.startActivity(context, intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if running on sdk 29 (android 10) or higher and {@link Manifest.permission#SYSTEM_ALERT_WINDOW}
|
||||
* permission has been granted or not.
|
||||
*
|
||||
* @param context The context for operations.
|
||||
* @param logResults If it should be logged that permission has been granted or not.
|
||||
* @return Returns {@code true} if permission is granted, otherwise {@code false}.
|
||||
*/
|
||||
public static boolean validateDisplayOverOtherAppsPermissionForPostAndroid10(@NonNull Context context,
|
||||
boolean logResults) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return true;
|
||||
|
||||
if (!checkDisplayOverOtherAppsPermission(context)) {
|
||||
if (logResults)
|
||||
Logger.logWarn(LOG_TAG, context.getPackageName() + " does not have Display over other apps (SYSTEM_ALERT_WINDOW) permission");
|
||||
return false;
|
||||
} else {
|
||||
if (logResults)
|
||||
Logger.logDebug(LOG_TAG, context.getPackageName() + " already has Display over other apps (SYSTEM_ALERT_WINDOW) permission");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Check if {@link Manifest.permission#REQUEST_IGNORE_BATTERY_OPTIMIZATIONS} permission has been
|
||||
* granted.
|
||||
*
|
||||
* @param context The context for operations.
|
||||
* @return Returns {@code true} if permission is granted, otherwise {@code false}.
|
||||
*/
|
||||
public static boolean checkIfBatteryOptimizationsDisabled(@NonNull Context context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
|
||||
return powerManager.isIgnoringBatteryOptimizations(context.getPackageName());
|
||||
} else
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Wrapper for {@link #requestDisableBatteryOptimizations(Context, int)}. */
|
||||
public static Error requestDisableBatteryOptimizations(@NonNull Context context) {
|
||||
return requestDisableBatteryOptimizations(context, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request user to grant {@link Manifest.permission#REQUEST_IGNORE_BATTERY_OPTIMIZATIONS}
|
||||
* permission to the app.
|
||||
*
|
||||
* @param context The context for operations, like an {@link Activity} or {@link Service} context.
|
||||
* It must be an instance of {@link Activity} or {@link AppCompatActivity} if
|
||||
* result is required via the Activity#onActivityResult() callback and
|
||||
* {@code requestCode} is `>=0`.
|
||||
* @param requestCode The request code to use while asking for permission. It must be `>=0` if
|
||||
* result it required.
|
||||
* @return Returns the {@code error} if requesting the permission was not successful, otherwise {@code null}.
|
||||
*/
|
||||
@SuppressLint("BatteryLife")
|
||||
public static Error requestDisableBatteryOptimizations(@NonNull Context context, int requestCode) {
|
||||
Logger.logInfo(LOG_TAG, "Requesting to disable battery optimizations");
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
|
||||
return null;
|
||||
|
||||
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||
intent.setData(Uri.parse("package:" + context.getPackageName()));
|
||||
|
||||
// Flag must not be passed for activity contexts, otherwise onActivityResult() will not be called with permission grant result.
|
||||
// Flag must be passed for non-activity contexts like services, otherwise "Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag" exception will be raised.
|
||||
if (!(context instanceof Activity))
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
if (requestCode >=0)
|
||||
return ActivityUtils.startActivityForResult(context, requestCode, intent);
|
||||
else
|
||||
return ActivityUtils.startActivity(context, intent);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
package com.termux.shared.android;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.shell.command.environment.AndroidShellEnvironment;
|
||||
import com.termux.shared.shell.command.ExecutionCommand;
|
||||
import com.termux.shared.shell.command.runner.app.AppShell;
|
||||
|
||||
/**
|
||||
* Utils for phantom processes added in android 12.
|
||||
*
|
||||
* https://github.com/termux/termux-app/issues/2366
|
||||
* https://issuetracker.google.com/u/1/issues/205156966#comment28
|
||||
* https://cs.android.com/android/_/android/platform/frameworks/base/+/09dcdad5
|
||||
* https://github.com/agnostic-apollo/Android-Docs/tree/master/ocs/apps/processes/phantom-cached-and-empty-processes.md
|
||||
*/
|
||||
public class PhantomProcessUtils {
|
||||
|
||||
private static final String LOG_TAG = "PhantomProcessUtils";
|
||||
|
||||
/**
|
||||
* If feature flag set to false, then will disable trimming of phantom process and processes using
|
||||
* excessive CPU. Flag is available on Pixel Android 12L beta 3 and Android 13. Availability on
|
||||
* other devices will depend on if other vendors merged the 09dcdad5 commit or not in their releases
|
||||
* and if they actually want to support the flag. Check {@link FeatureFlagUtils} javadocs for
|
||||
* more details.
|
||||
*/
|
||||
public static final String FEATURE_FLAG_SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS = "settings_enable_monitor_phantom_procs";
|
||||
|
||||
/**
|
||||
* Maximum number of allowed phantom processes. It is also used as the label for the currently
|
||||
* enforced ActivityManagerConstants MAX_PHANTOM_PROCESSES value in the `dumpsys activity settings`
|
||||
* output.
|
||||
*
|
||||
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r4:frameworks/base/services/core/java/com/android/server/am/ActivityManagerConstants.java;l=574
|
||||
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r4:frameworks/base/services/core/java/com/android/server/am/ActivityManagerConstants.java;l=172
|
||||
*/
|
||||
public static final String KEY_MAX_PHANTOM_PROCESSES = "max_phantom_processes";
|
||||
|
||||
/**
|
||||
* Whether or not syncs (bulk set operations) for DeviceConfig are disabled currently. The value
|
||||
* is boolean (1 or 0). The value '1' means that DeviceConfig#setProperties(DeviceConfig.Properties)
|
||||
* will return {@code false}.
|
||||
*
|
||||
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r4:frameworks/base/core/java/android/provider/DeviceConfig.java
|
||||
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r4:frameworks/base/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java;l=1186
|
||||
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r4:frameworks/base/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java;l=1142
|
||||
*/
|
||||
public static final String SETTINGS_GLOBAL_DEVICE_CONFIG_SYNC_DISABLED = "device_config_sync_disabled";
|
||||
|
||||
/**
|
||||
* Get {@link #FEATURE_FLAG_SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS} feature flag value.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @return Returns {@link FeatureFlagUtils.FeatureFlagValue}.
|
||||
*/
|
||||
@NonNull
|
||||
public static FeatureFlagUtils.FeatureFlagValue getFeatureFlagMonitorPhantomProcsValueString(@NonNull Context context) {
|
||||
return FeatureFlagUtils.getFeatureFlagValueString(context, FEATURE_FLAG_SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get currently enforced ActivityManagerConstants MAX_PHANTOM_PROCESSES value, defaults to 32.
|
||||
* Can be changed by modifying device config activity_manager namespace "max_phantom_processes" value.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @return Returns {@link Integer}.
|
||||
*/
|
||||
@Nullable
|
||||
public static Integer getActivityManagerMaxPhantomProcesses(@NonNull Context context) {
|
||||
if (!PermissionUtils.checkPermissions(context, new String[]{Manifest.permission.DUMP, Manifest.permission.PACKAGE_USAGE_STATS})) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Dumpsys logs the currently enforced MAX_PHANTOM_PROCESSES value and not the device config setting.
|
||||
String script = "/system/bin/dumpsys activity settings | /system/bin/grep -iE '^[\t ]+" + KEY_MAX_PHANTOM_PROCESSES + "=[0-9]+$' | /system/bin/cut -d = -f2";
|
||||
ExecutionCommand executionCommand = new ExecutionCommand(-1, "/system/bin/sh", null,
|
||||
script + "\n", "/", ExecutionCommand.Runner.APP_SHELL.getName(), true);
|
||||
executionCommand.commandLabel = " ActivityManager " + KEY_MAX_PHANTOM_PROCESSES + " Command";
|
||||
executionCommand.backgroundCustomLogLevel = Logger.LOG_LEVEL_OFF;
|
||||
AppShell appShell = AppShell.execute(context, executionCommand, null, new AndroidShellEnvironment(), null, true);
|
||||
boolean stderrSet = !executionCommand.resultData.stderr.toString().isEmpty();
|
||||
if (appShell == null || !executionCommand.isSuccessful() || executionCommand.resultData.exitCode != 0 || stderrSet) {
|
||||
Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return Integer.parseInt(executionCommand.resultData.stdout.toString().trim());
|
||||
} catch (NumberFormatException e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "The " + executionCommand.commandLabel + " did not return a valid integer", e);
|
||||
Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get {@link #SETTINGS_GLOBAL_DEVICE_CONFIG_SYNC_DISABLED} settings value.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @return Returns {@link Integer}.
|
||||
*/
|
||||
@Nullable
|
||||
public static Integer getSettingsGlobalDeviceConfigSyncDisabled(@NonNull Context context) {
|
||||
return (Integer) SettingsProviderUtils.getSettingsValue(context, SettingsProviderUtils.SettingNamespace.GLOBAL,
|
||||
SettingsProviderUtils.SettingType.INT, SETTINGS_GLOBAL_DEVICE_CONFIG_SYNC_DISABLED, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
package com.termux.shared.android;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ProcessUtils {
|
||||
|
||||
public static final String LOG_TAG = "ProcessUtils";
|
||||
|
||||
/**
|
||||
* Get the app process name for a pid with a call to {@link ActivityManager#getRunningAppProcesses()}.
|
||||
*
|
||||
* This will not return child process names. Android did not keep track of them before android 12
|
||||
* phantom process addition, but there is no API via IActivityManager to get them.
|
||||
*
|
||||
* To get process name for pids of own app's child processes, check `get_process_name_from_cmdline()`
|
||||
* in `local-socket.cpp`.
|
||||
*
|
||||
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/app/ActivityManager.java;l=3362
|
||||
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java;l=8434
|
||||
* https://cs.android.com/android/_/android/platform/frameworks/base/+/refs/tags/android-12.0.0_r32:services/core/java/com/android/server/am/PhantomProcessList.java
|
||||
* https://cs.android.com/android/_/android/platform/frameworks/base/+/refs/tags/android-12.0.0_r32:services/core/java/com/android/server/am/PhantomProcessRecord.java
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param pid The pid of the process.
|
||||
* @return Returns the app process name if found, otherwise {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getAppProcessNameForPid(@NonNull Context context, int pid) {
|
||||
if (pid < 0) return null;
|
||||
|
||||
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
if (activityManager == null) return null;
|
||||
try {
|
||||
List<ActivityManager.RunningAppProcessInfo> runningApps = activityManager.getRunningAppProcesses();
|
||||
if (runningApps == null) {
|
||||
return null;
|
||||
}
|
||||
for (ActivityManager.RunningAppProcessInfo procInfo : runningApps) {
|
||||
if (procInfo.pid == pid) {
|
||||
return procInfo.processName;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get app process name for pid " + pid, e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
package com.termux.shared.android;
|
||||
|
||||
import android.content.Context;
|
||||
import android.provider.Settings;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
|
||||
public class SettingsProviderUtils {
|
||||
|
||||
private static final String LOG_TAG = "SettingsProviderUtils";
|
||||
|
||||
/** The namespaces for {@link Settings} provider. */
|
||||
public enum SettingNamespace {
|
||||
/** The {@link Settings.Global} namespace */
|
||||
GLOBAL,
|
||||
|
||||
/** The {@link Settings.Secure} namespace */
|
||||
SECURE,
|
||||
|
||||
/** The {@link Settings.System} namespace */
|
||||
SYSTEM
|
||||
}
|
||||
|
||||
/** The type of values for {@link Settings} provider. */
|
||||
public enum SettingType {
|
||||
FLOAT,
|
||||
INT,
|
||||
LONG,
|
||||
STRING,
|
||||
URI
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings key value from {@link SettingNamespace} namespace and of {@link SettingType} type.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param namespace The {@link SettingNamespace} in which to get key value from.
|
||||
* @param type The {@link SettingType} for the key.
|
||||
* @param key The {@link String} name for key.
|
||||
* @param def The {@link Object} default value for key.
|
||||
* @return Returns the key value. This will be {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static Object getSettingsValue(@NonNull Context context, @NonNull SettingNamespace namespace,
|
||||
@NonNull SettingType type, @NonNull String key, @Nullable Object def) {
|
||||
try {
|
||||
switch (namespace) {
|
||||
case GLOBAL:
|
||||
switch (type) {
|
||||
case FLOAT:
|
||||
return Settings.Global.getFloat(context.getContentResolver(), key);
|
||||
case INT:
|
||||
return Settings.Global.getInt(context.getContentResolver(), key);
|
||||
case LONG:
|
||||
return Settings.Global.getLong(context.getContentResolver(), key);
|
||||
case STRING:
|
||||
return Settings.Global.getString(context.getContentResolver(), key);
|
||||
case URI:
|
||||
return Settings.Global.getUriFor(key);
|
||||
}
|
||||
case SECURE:
|
||||
switch (type) {
|
||||
case FLOAT:
|
||||
return Settings.Secure.getFloat(context.getContentResolver(), key);
|
||||
case INT:
|
||||
return Settings.Secure.getInt(context.getContentResolver(), key);
|
||||
case LONG:
|
||||
return Settings.Secure.getLong(context.getContentResolver(), key);
|
||||
case STRING:
|
||||
return Settings.Secure.getString(context.getContentResolver(), key);
|
||||
case URI:
|
||||
return Settings.Secure.getUriFor(key);
|
||||
}
|
||||
case SYSTEM:
|
||||
switch (type) {
|
||||
case FLOAT:
|
||||
return Settings.System.getFloat(context.getContentResolver(), key);
|
||||
case INT:
|
||||
return Settings.System.getInt(context.getContentResolver(), key);
|
||||
case LONG:
|
||||
return Settings.System.getLong(context.getContentResolver(), key);
|
||||
case STRING:
|
||||
return Settings.System.getString(context.getContentResolver(), key);
|
||||
case URI:
|
||||
return Settings.System.getUriFor(key);
|
||||
}
|
||||
}
|
||||
} catch (Settings.SettingNotFoundException e) {
|
||||
// Ignore
|
||||
} catch (Exception e) {
|
||||
Logger.logError(LOG_TAG, "Failed to get \"" + key + "\" key value from settings \"" + namespace.name() + "\" namespace of type \"" + type.name() + "\"");
|
||||
}
|
||||
return def;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
package com.termux.shared.android;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.reflection.ReflectionUtils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class UserUtils {
|
||||
|
||||
public static final String LOG_TAG = "UserUtils";
|
||||
|
||||
/**
|
||||
* Get the user name for user id with a call to {@link #getNameForUidFromPackageManager(Context, int)}
|
||||
* and if that fails, then a call to {@link #getNameForUidFromLibcore(int)}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param uid The user id.
|
||||
* @return Returns the user name if found, otherwise {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getNameForUid(@NonNull Context context, int uid) {
|
||||
String name = getNameForUidFromPackageManager(context, uid);
|
||||
if (name == null)
|
||||
name = getNameForUidFromLibcore(uid);
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user name for user id with a call to {@link PackageManager#getNameForUid(int)}.
|
||||
*
|
||||
* This will not return user names for non app user id like for root user 0, use {@link #getNameForUidFromLibcore(int)}
|
||||
* to get those.
|
||||
*
|
||||
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/content/pm/PackageManager.java;l=5556
|
||||
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/app/ApplicationPackageManager.java;l=1028
|
||||
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java;l=10293
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param uid The user id.
|
||||
* @return Returns the user name if found, otherwise {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getNameForUidFromPackageManager(@NonNull Context context, int uid) {
|
||||
if (uid < 0) return null;
|
||||
|
||||
try {
|
||||
String name = context.getPackageManager().getNameForUid(uid);
|
||||
if (name != null && name.endsWith(":" + uid))
|
||||
name = name.replaceAll(":" + uid + "$", ""); // Remove ":<uid>" suffix
|
||||
return name;
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get name for uid \"" + uid + "\" from package manager", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user name for user id with a call to `Libcore.os.getpwuid()`.
|
||||
*
|
||||
* This will return user names for non app user id like for root user 0 as well, but this call
|
||||
* is expensive due to usage of reflection, and requires hidden API bypass, check
|
||||
* {@link ReflectionUtils#bypassHiddenAPIReflectionRestrictions()} for details.
|
||||
*
|
||||
* `BlockGuardOs` implements the `Os` interface and its instance is stored in `Libcore` class static `os` field.
|
||||
* The `getpwuid` method is implemented by `ForwardingOs`, which is the super class of `BlockGuardOs`.
|
||||
* The `getpwuid` method returns `StructPasswd` object whose `pw_name` contains the user name for id.
|
||||
*
|
||||
* https://stackoverflow.com/a/28057167/14686958
|
||||
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:libcore/luni/src/main/java/libcore/io/Libcore.java;l=39
|
||||
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:libcore/luni/src/main/java/libcore/io/Os.java;l=279
|
||||
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:libcore/luni/src/main/java/libcore/io/BlockGuardOs.java
|
||||
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:libcore/luni/src/main/java/libcore/io/ForwardingOs.java;l=340
|
||||
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:libcore/luni/src/main/java/android/system/StructPasswd.java
|
||||
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:bionic/libc/bionic/grp_pwd.cpp;l=553
|
||||
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:system/core/libcutils/include/private/android_filesystem_config.h;l=43
|
||||
*
|
||||
* @param uid The user id.
|
||||
* @return Returns the user name if found, otherwise {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getNameForUidFromLibcore(int uid) {
|
||||
if (uid < 0) return null;
|
||||
|
||||
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
|
||||
try {
|
||||
String libcoreClassName = "libcore.io.Libcore";
|
||||
Class<?> clazz = Class.forName(libcoreClassName);
|
||||
Object os; // libcore.io.BlockGuardOs
|
||||
try {
|
||||
os = ReflectionUtils.invokeField(Class.forName(libcoreClassName), "os", null).value;
|
||||
} catch (Exception e) {
|
||||
// ClassCastException may be thrown
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get \"os\" field value for " + libcoreClassName + " class", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (os == null) {
|
||||
Logger.logError(LOG_TAG, "Failed to get BlockGuardOs class obj from Libcore");
|
||||
return null;
|
||||
}
|
||||
|
||||
clazz = os.getClass().getSuperclass(); // libcore.io.ForwardingOs
|
||||
if (clazz == null) {
|
||||
Logger.logError(LOG_TAG, "Failed to find super class ForwardingOs from object of class " + os.getClass().getName());
|
||||
return null;
|
||||
}
|
||||
|
||||
Object structPasswd; // android.system.StructPasswd
|
||||
try {
|
||||
Method getpwuidMethod = ReflectionUtils.getDeclaredMethod(clazz, "getpwuid", int.class);
|
||||
if (getpwuidMethod == null) return null;
|
||||
structPasswd = ReflectionUtils.invokeMethod(getpwuidMethod, os, uid).value;
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to invoke getpwuid() method of " + clazz.getName() + " class", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (structPasswd == null) {
|
||||
Logger.logError(LOG_TAG, "Failed to get StructPasswd obj from call to ForwardingOs.getpwuid()");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
clazz = structPasswd.getClass();
|
||||
return (String) ReflectionUtils.invokeField(clazz, "pw_name", structPasswd).value;
|
||||
} catch (Exception e) {
|
||||
// ClassCastException may be thrown
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get \"pw_name\" field value for " + clazz.getName() + " class", e);
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get name for uid \"" + uid + "\" from Libcore", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue