Compare commits

..

1 Commits

Author SHA1 Message Date
Fredrik Fornwall 60f0771888
[WIP] Introduce an 'updated' APK packages flavor 2021-05-12 20:23:39 +02:00
287 changed files with 6795 additions and 28035 deletions

8
.gitattributes vendored
View File

@ -1,5 +1,5 @@
* text=auto
*.bat text eol=crlf
*.gradle text eol=lf
*.mk text eol=lf
*.sh text eol=lf
*.bat eol=crlf
*.gradle eol=lf
*.mk eol=lf
*.sh eol=lf

3
.github/FUNDING.yml vendored
View File

@ -1 +1,2 @@
custom: https://termux.dev/donate
patreon: termux
custom: https://paypal.me/fornwall

View File

@ -1,44 +0,0 @@
name: "Bug report"
description: "Create a report to help us improve"
title: "[Bug]: "
labels: ["bug report"]
body:
- type: markdown
attributes:
value: |
This is a bug tracker of the Termux app. If you have issues with a package inside the app, then please open an issue at [termux-packages](https://github.com/termux/termux-packages) instead.
Use search before you open an issue to check whether your issue has been already reported and perhaps solved.
Android versions 5.x and 6.x are not supported anymore.
If you have issues installing packages then please see https://github.com/termux/termux-packages/issues/6726.
- type: textarea
attributes:
label: Problem description
description: |
A clear and concise description of what the problem is. You may attach the logs, screenshots, screen video recording and whatever else that will help to understand the issue.
Issues without proper description will be closed without solution.
validations:
required: true
- type: textarea
attributes:
label: Steps to reproduce the behavior.
description: |
Please post all necessary commands that are needed to reproduce the issue.
validations:
required: true
- type: textarea
attributes:
label: What is the expected behavior?
- type: textarea
attributes:
label: System information
description: Please provide info about your device
value: |
* Termux application version:
* Android OS version:
* Device model:
validations:
required: true

View File

@ -1,19 +0,0 @@
name: "Feature request"
description: "Suggest a new feature for Termux application"
title: "[Feature]: "
labels: ["feature request"]
body:
- type: textarea
attributes:
label: Feature description
description: Describe the feature and why you want it.
validations:
required: true
- type: textarea
attributes:
label: Additional information
description: |
Does another app/terminal emulator have this feature?
Provide links to more background information.
validations:
required: true

35
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,35 @@
---
name: Bug report
about: Create a report to help us improve Termux application
---
<!--
IMPORTANT:
1. Support of Android 5.x - 6.x is finished.
2. Fill the template AFTER comments.
-->
**Problem description**
<!--
A clear and concise description of what the problem is.
You may post screenshots in addition to description.
-->
**Steps to reproduce**
<!--
Steps to reproduce the behavior. Please post all necessary
commands that are needed to reproduce the issue.
-->
**Expected behavior**
<!--
A clear and concise description of what you expected to happen.
-->
**Additional information**
* Termux application version:
* Android OS version:
* Device model:

View File

@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Want ask questions about the project?
url: https://github.com/termux/termux-app/discussions
about: Join GitHub Discussions

View File

@ -0,0 +1,22 @@
---
name: Feature request
about: Suggest a new feature for Termux application
---
<!--
IMPORTANT:
1. Support of Android 5.x - 6.x is finished.
2. Fill the template AFTER comments.
-->
**Feature description**
<!--
Describe the feature and why you want it.
-->
**Reference implementation**
Does another app/terminal emulator have this feature?
Provide links to more background information.

View File

@ -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"

View File

@ -1,84 +0,0 @@
name: Attach Debug APKs To Release
on:
release:
types:
- published
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
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"
echo "Deleting '$RELEASE_VERSION_NAME' release and '$GITHUB_REF' tag"
hub release delete "$RELEASE_VERSION_NAME"
git push --delete origin "$GITHUB_REF"
exit 1
}
echo "Setting vars"
RELEASE_VERSION_NAME="${GITHUB_REF/refs\/tags\//}"
if ! printf "%s" "${RELEASE_VERSION_NAME/v/}" | grep -qP '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'; then
exit_on_error "The versionName '${RELEASE_VERSION_NAME/v/}' is not a valid version as per semantic version '2.0.0' spec in the format 'major.minor.patch(-prerelease)(+buildmetadata)'. https://semver.org/spec/v2.0.0.html."
fi
APK_DIR_PATH="./app/build/outputs/apk/debug"
APK_VERSION_TAG="$RELEASE_VERSION_NAME+${{ env.PACKAGE_VARIANT }}-github-debug"
APK_BASENAME_PREFIX="termux-app_$APK_VERSION_TAG"
echo "Building APKs for 'APK_VERSION_TAG' release"
export TERMUX_APK_VERSION_TAG="$APK_VERSION_TAG" # Used by app/build.gradle
export TERMUX_PACKAGE_VARIANT="${{ env.PACKAGE_VARIANT }}" # Used by app/build.gradle
if ! ./gradlew assembleDebug; then
exit_on_error "Build failed for '$APK_VERSION_TAG' release."
fi
echo "Validating APKs"
for abi in universal arm64-v8a armeabi-v7a x86_64 x86; do
if ! test -f "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_$abi.apk"; then
files_found="$(ls "$APK_DIR_PATH")"
exit_on_error "Failed to find built APK at '$APK_DIR_PATH/${APK_BASENAME_PREFIX}_$abi.apk'. Files found: "$'\n'"$files_found"
fi
done
echo "Generating sha25sums file"
if ! (cd "$APK_DIR_PATH"; sha256sum \
"${APK_BASENAME_PREFIX}_universal.apk" \
"${APK_BASENAME_PREFIX}_arm64-v8a.apk" \
"${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."
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."
fi

View File

@ -4,130 +4,23 @@ on:
push:
branches:
- master
- 'github-releases/**'
- android-10
pull_request:
branches:
- master
- android-10
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'
- 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/")"
RELEASE_VERSION_NAME="v$CURRENT_VERSION_NAME+${GITHUB_SHA:0:7}" # The "+" is necessary so that versioning precedence is not affected
if ! printf "%s" "${RELEASE_VERSION_NAME/v/}" | grep -qP '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'; then
exit_on_error "The versionName '${RELEASE_VERSION_NAME/v/}' is not a valid version as per semantic version '2.0.0' spec in the format 'major.minor.patch(-prerelease)(+buildmetadata)'. https://semver.org/spec/v2.0.0.html."
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_BASENAME_PREFIX="termux-app_$APK_VERSION_TAG"
# Used by attachment steps later
echo "APK_DIR_PATH=$APK_DIR_PATH" >> $GITHUB_ENV
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"
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."
fi
echo "Validating APKs"
for abi in universal arm64-v8a armeabi-v7a x86_64 x86; do
if ! test -f "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_$abi.apk"; then
files_found="$(ls "$APK_DIR_PATH")"
exit_on_error "Failed to find built APK at '$APK_DIR_PATH/${APK_BASENAME_PREFIX}_$abi.apk'. Files found: "$'\n'"$files_found"
fi
done
echo "Generating sha25sums file"
if ! (cd "$APK_DIR_PATH"; sha256sum \
"${APK_BASENAME_PREFIX}_universal.apk" \
"${APK_BASENAME_PREFIX}_arm64-v8a.apk" \
"${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."
fi
- name: Attach universal APK file
uses: actions/upload-artifact@v6
with:
name: ${{ env.APK_BASENAME_PREFIX }}_universal
path: |
${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_universal.apk
${{ env.APK_DIR_PATH }}/output-metadata.json
- name: Attach arm64-v8a APK file
uses: actions/upload-artifact@v6
with:
name: ${{ env.APK_BASENAME_PREFIX }}_arm64-v8a
path: |
${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_arm64-v8a.apk
${{ env.APK_DIR_PATH }}/output-metadata.json
- name: Attach armeabi-v7a APK file
uses: actions/upload-artifact@v6
with:
name: ${{ env.APK_BASENAME_PREFIX }}_armeabi-v7a
path: |
${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_armeabi-v7a.apk
${{ env.APK_DIR_PATH }}/output-metadata.json
- name: Attach x86_64 APK file
uses: actions/upload-artifact@v6
with:
name: ${{ env.APK_BASENAME_PREFIX }}_x86_64
path: |
${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_x86_64.apk
${{ env.APK_DIR_PATH }}/output-metadata.json
- name: Attach x86 APK file
uses: actions/upload-artifact@v6
with:
name: ${{ env.APK_BASENAME_PREFIX }}_x86
path: |
${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_x86.apk
${{ env.APK_DIR_PATH }}/output-metadata.json
- name: Attach sha256sums file
uses: actions/upload-artifact@v6
with:
name: ${{ env.APK_BASENAME_PREFIX }}_sha256sums
path: |
${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_sha256sums
${{ env.APK_DIR_PATH }}/output-metadata.json
- name: Clone repository
uses: actions/checkout@v2
- name: Build
run: |
./gradlew assembleDebug
- name: Store generated APK file
uses: actions/upload-artifact@v2
with:
name: termux-app
path: ./app/build/outputs/apk/debug

View File

@ -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

View File

@ -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@v2
- uses: gradle/wrapper-validation-action@v1

26
.github/workflows/publish_libraries.yml vendored Normal file
View File

@ -0,0 +1,26 @@
name: Publish library packages
on:
push:
branches:
- master
paths:
- 'terminal-emulator/build.gradle'
- 'terminal-view/build.gradle'
- 'termux-shared/build.gradle'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v2
- name: Perform release build
run: |
./gradlew assembleRelease
- name: Publish libraries on Github Packages
env:
GH_USERNAME: xeffyr
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
./gradlew publish

View File

@ -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@v2
- name: Execute tests
run: |
./gradlew test

View File

@ -1,21 +0,0 @@
name: Trigger Termux Library Builds on Jitpack
on:
release:
types:
- published
jobs:
trigger-termux-library-builds:
runs-on: ubuntu-latest
steps:
- name: Set vars
run: echo "TERMUX_LIB_VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_ENV # Do not include "v" prefix
- name: Echo release
run: echo "Triggering termux library builds on jitpack for '$TERMUX_LIB_VERSION' release after waiting for 3 mins"
- name: Trigger termux library builds on jitpack
run: |
sleep 180 # It will take some time for the new tag to be detected by Jitpack
curl --max-time 600 --no-progress-meter "https://jitpack.io/com/termux/termux-app/terminal-emulator/$TERMUX_LIB_VERSION/terminal-emulator-$TERMUX_LIB_VERSION.pom"
curl --max-time 600 --no-progress-meter "https://jitpack.io/com/termux/termux-app/terminal-view/$TERMUX_LIB_VERSION/terminal-view-$TERMUX_LIB_VERSION.pom"
curl --max-time 600 --no-progress-meter "https://jitpack.io/com/termux/termux-app/termux-shared/$TERMUX_LIB_VERSION/termux-shared-$TERMUX_LIB_VERSION.pom"

8
.gitignore vendored
View File

@ -4,13 +4,15 @@
# Built application files
build/
release/
*.apk
*.so
.externalNativeBuild
.cxx
*.zip
# Crashlytics configuations
com_crashlytics_export_strings.xml
# Local configuration file (sdk path, etc)
local.properties
@ -24,10 +26,6 @@ local.properties
.idea/
*.iml
# Vim
*.swo
*.swp
# OS-specific files
.DS_Store
.DS_Store?

View File

@ -2,5 +2,6 @@ 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.
- Check [`termux-shared/LICENSE.md`](termux-shared/LICENSE.md) for `termux-shared` library related exceptions.
- [Terminal Emulator for Android](https://github.com/jackpal/Android-Terminal-Emulator) code is used which is released under [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). Check [terminal-view](terminal-view) and [terminal-emulator](terminal-emulator) modules.
- [libcore/ojluni](https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/) code is used which is released under [GPLv2 only with "Classpath" exception](https://openjdk.java.net/legal/gplv2+ce.html). Check `com.termux.shared.file` package under [termux-shared](termux-shared) module.
- [libsuperuser ](https://github.com/Chainfire/libsuperuser) code is used which is released under [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). Check `com.termux.shared.shell.StreamGobbler` class under [termux-shared](termux-shared) module.

224
README.md
View File

@ -3,9 +3,6 @@
[![Build status](https://github.com/termux/termux-app/workflows/Build/badge.svg)](https://github.com/termux/termux-app/actions)
[![Testing status](https://github.com/termux/termux-app/workflows/Unit%20tests/badge.svg)](https://github.com/termux/termux-app/actions)
[![Join the chat at https://gitter.im/termux/termux](https://badges.gitter.im/termux/termux.svg)](https://gitter.im/termux/termux)
[![Join the Termux discord server](https://img.shields.io/discord/641256914684084234.svg?label=&logo=discord&logoColor=ffffff&color=5865F2)](https://discord.gg/HXpF69X)
[![Termux library releases at Jitpack](https://jitpack.io/v/termux/termux-app.svg)](https://jitpack.io/#termux/termux-app)
[Termux](https://termux.com) is an Android terminal application and Linux environment.
@ -13,23 +10,20 @@ 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 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)
- [For Devs and Contributors](#For-Devs-and-Contributors)
##
@ -50,88 +44,38 @@ The core [Termux](https://github.com/termux/termux-app) app comes with the follo
## Installation
Latest version is `v0.118.3`.
Termux can be obtained through various sources listed below for **only** Android `>= 7`. Support was dropped for Android `5` and `6` on [2020-01-01](https://www.reddit.com/r/termux/comments/dnzdbs/end_of_android56_support_on_20200101/) at `v0.83`, old builds are available on [archive.org](https://archive.org/details/termux-repositories-legacy).
**NOTICE: It is highly recommended that you update to `v0.118.0` or higher ASAP for various bug fixes, including a critical world-readable vulnerability reported [here](https://termux.github.io/general/2022/02/15/termux-apps-vulnerability-disclosures.html). See [below](#google-play-store-experimental-branch) for information regarding Termux on Google Play.**
The APK files of different sources are signed with different signature keys. The `Termux` app and all its plugins use the same [sharedUserId](https://developer.android.com/guide/topics/manifest/manifest-element) `com.termux` and so all their APKs installed on a device must have been signed with the same signature key to work together and so they must all be installed from the same source. Do not attempt to mix them together, i.e do not try to install an app or plugin from F-Droid and another one from a different source. Android Package Manager will also normally not allow installation of APKs with a different signatures and you will get errors on installation like `App not installed`, `Failed to install due to an unknown error`, `INSTALL_FAILED_UPDATE_INCOMPATIBLE`, `INSTALL_FAILED_SHARED_USER_INCOMPATIBLE`, `signatures do not match previously installed version`, etc. This restriction can be bypassed with root or with custom roms.
Termux can be obtained through various sources listed below for **only** Android `>= 7` with full support for apps and packages.
Support for both app and packages was dropped for Android `5` and `6` on [2020-01-01](https://www.reddit.com/r/termux/comments/dnzdbs/end_of_android56_support_on_20200101/) at `v0.83`, however it was re-added just for the app *without any support for package updates* on [2022-05-24](https://github.com/termux/termux-app/pull/2740) via the [GitHub](#github) sources. Check [here](https://github.com/termux/termux-app/wiki/Termux-on-android-5-or-6) for the details.
The APK files of different sources are signed with different signature keys. The `Termux` app and all its plugins use the same [`sharedUserId`](https://developer.android.com/guide/topics/manifest/manifest-element) `com.termux` and so all their APKs installed on a device must have been signed with the same signature key to work together and so they must all be installed from the same source. Do not attempt to mix them together, i.e do not try to install an app or plugin from `F-Droid` and another one from a different source like `GitHub`. Android Package Manager will also normally not allow installation of APKs with different signatures and you will get errors on installation like `App not installed`, `Failed to install due to an unknown error`, `INSTALL_FAILED_UPDATE_INCOMPATIBLE`, `INSTALL_FAILED_SHARED_USER_INCOMPATIBLE`, `signatures do not match previously installed version`, etc. This restriction can be bypassed with root or with custom roms.
If you wish to install from a different source, then you must **uninstall any and all existing Termux or its plugin app APKs** from your device first, then install all new APKs from the same new source. Check [Uninstallation](#uninstallation) section for details. You may also want to consider [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux) before the uninstallation so that you can restore it after re-installing from Termux different source.
In the following paragraphs, *"bootstrap"* refers to the minimal packages that are shipped with the `termux-app` itself to start a working shell environment. Its zips are built and released [here](https://github.com/termux/termux-packages/releases).
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 uninstallation.
### F-Droid
Termux application can be obtained from `F-Droid` from [here](https://f-droid.org/en/packages/com.termux/).
Termux application can be obtained from F-Droid [here](https://f-droid.org/en/packages/com.termux/). 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. F-Droid releases are built and published by F-Droid once they detect a new Github release. The Termux maintainers **do not** have any control over building and publishing of Termux app 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.
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.
### Debug Builds
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.
For users who don't want to wait for F-Droid releases and want to try out the latest features immediately or want to test their pull requests can get the APKs from [Github Actions](https://github.com/termux/termux-app/actions) page from the workflow runs labeled `Build`. The APK will be listed under `Artifacts` section. These are published for each commit done to the repository. These APKs are [debuggable](https://developer.android.com/studio/debug) and are also not compatible with other sources.
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.
### Google Playstore **(Deprecated)**
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.
**Termux and its plugins are no longer updated on [Google playstore](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).** The last version released for Android `>= 7` was `v0.101`. There are currently no immediate plans to resume updates on Google playstore. **It is highly recommended to not install Termux from playstore for now.** Any current users **should switch** to a different source like F-Droid.
### GitHub
If for some reason you don't want to switch, 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.
##
Termux application can be obtained on `GitHub` either from [`GitHub Releases`](https://github.com/termux/termux-app/releases) for version `>= 0.118.0` or from [`GitHub Build Action`](https://github.com/termux/termux-app/actions/workflows/debug_build.yml?query=branch%3Amaster+event%3Apush) workflows. **For android `>= 7`, only install `apt-android-7` variants. For android `5` and `6`, only install `apt-android-5` variants.**
The APKs for `GitHub Releases` will be listed under `Assets` drop-down of a release. These are automatically attached when a new version is released.
The APKs for `GitHub 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.
The [test key](https://github.com/termux/termux-app/blob/master/app/testkey_untrusted.jks) shall not be used to impersonate @termux and can't be used for this anyway. This key is not trusted by us and it is quite easy to detect its use in user generated content.
<details>
<summary>Keystore information</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
```
</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 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 its available on your device and search `termux` in the applications list.
Go to `Android Settings` -> `Applications` and then look for those apps. You can also use the search feature if its available on your device and search `termux` in the applications list.
Even if you think you have not installed any of the plugins, it's strongly suggested to go through the application list in Android settings and double-check.
Even if you think you have not installed any of the plugins, its strongly suggesting to go through the application list in Android settings and double check.
##
@ -144,10 +88,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](http://twitter.com/termux/)
- [Termux Reports Email](mailto:termuxreports@groups.io)
### Wikis
@ -160,136 +104,42 @@ The main ones are the following.
- [Termux File System Layout](https://github.com/termux/termux-packages/wiki/Termux-file-system-layout)
- [Differences From Linux](https://wiki.termux.com/wiki/Differences_from_Linux)
- [Package Management](https://wiki.termux.com/wiki/Package_Management)
- [Remote Access](https://wiki.termux.com/wiki/Remote_Access)
- [Remote_Access](https://wiki.termux.com/wiki/Remote_Access)
- [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux)
- [Terminal Settings](https://wiki.termux.com/wiki/Terminal_Settings)
- [Touch Keyboard](https://wiki.termux.com/wiki/Touch_Keyboard)
- [Android Storage and Sharing Data with Other Apps](https://wiki.termux.com/wiki/Internal_and_external_storage)
- [Android APIs](https://wiki.termux.com/wiki/Termux:API)
- [Moved Termux Packages Hosting From Bintray to IPFS](https://github.com/termux/termux-packages/issues/6348)
- [Running Commands in Termux From Other Apps via `RUN_COMMAND` intent](https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent)
- [Termux and Android 10](https://github.com/termux/termux-packages/wiki/Termux-and-Android-10)
### Terminal
<details>
<summary></summary>
### Terminal resources
- [XTerm control sequences](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
- [vt100.net](https://vt100.net/)
- [Terminal codes (ANSI and terminfo equivalents)](https://wiki.bash-hackers.org/scripting/terminalcodes)
- [XTerm control sequences](http://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
- [vt100.net](http://vt100.net/)
- [Terminal codes (ANSI and terminfo equivalents)](http://wiki.bash-hackers.org/scripting/terminalcodes)
### Terminal emulators
- VTE (libvte): Terminal emulator widget for GTK+, mainly used in gnome-terminal. [Source](https://github.com/GNOME/vte), [Open Issues](https://bugzilla.gnome.org/buglist.cgi?quicksearch=product%3A%22vte%22+), and [All (including closed) issues](https://bugzilla.gnome.org/buglist.cgi?bug_status=RESOLVED&bug_status=VERIFIED&chfield=resolution&chfieldfrom=-2000d&chfieldvalue=FIXED&product=vte&resolution=FIXED).
- iTerm 2: OS X terminal application. [Source](https://github.com/gnachman/iTerm2), [Issues](https://gitlab.com/gnachman/iterm2/issues) and [Documentation](https://iterm2.com/documentation.html) (which includes [iTerm2 proprietary escape codes](https://iterm2.com/documentation-escape-codes.html)).
- iTerm 2: OS X terminal application. [Source](https://github.com/gnachman/iTerm2), [Issues](https://gitlab.com/gnachman/iterm2/issues) and [Documentation](http://www.iterm2.com/documentation.html) (which includes [iTerm2 proprietary escape codes](http://www.iterm2.com/documentation-escape-codes.html)).
- Konsole: KDE terminal application. [Source](https://projects.kde.org/projects/kde/applications/konsole/repository), in particular [tests](https://projects.kde.org/projects/kde/applications/konsole/repository/revisions/master/show/tests), [Bugs](https://bugs.kde.org/buglist.cgi?bug_severity=critical&bug_severity=grave&bug_severity=major&bug_severity=crash&bug_severity=normal&bug_severity=minor&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole) and [Wishes](https://bugs.kde.org/buglist.cgi?bug_severity=wishlist&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole).
- hterm: JavaScript terminal implementation from Chromium. [Source](https://github.com/chromium/hterm), including [tests](https://github.com/chromium/hterm/blob/master/js/hterm_vt_tests.js), and [Google group](https://groups.google.com/a/chromium.org/forum/#!forum/chromium-hterm).
- xterm: The grandfather of terminal emulators. [Source](https://invisible-island.net/datafiles/release/xterm.tar.gz).
- xterm: The grandfather of terminal emulators. [Source](http://invisible-island.net/datafiles/release/xterm.tar.gz).
- Connectbot: Android SSH client. [Source](https://github.com/connectbot/connectbot)
- Android Terminal Emulator: Android terminal app which Termux terminal handling is based on. Inactive. [Source](https://github.com/jackpal/Android-Terminal-Emulator).
</details>
##
### Debugging
## For Devs and Contributors
You can help debug problems of the `Termux` app and its plugins by setting appropriate `logcat` `Log Level` in `Termux` app settings -> `<APP_NAME>` -> `Debugging` -> `Log Level` (Requires `Termux` app version `>= 0.118.0`). The `Log Level` defaults to `Normal` and log level `Verbose` currently logs additional information. Its best to revert log level to `Normal` after you have finished debugging since private data may otherwise be passed to `logcat` during normal operation and moreover, additional logging increases execution time.
The plugin apps **do not execute the commands themselves** but send execution intents to `Termux` app, which has its own log level which can be set in `Termux` app settings -> `Termux` -> `Debugging` -> `Log Level`. So you must set log level for both `Termux` and the respective plugin app settings to get all the info.
Once log levels have been set, you can run the `logcat` command in `Termux` app terminal to view the logs in realtime (`Ctrl+c` to stop) or use `logcat -d > logcat.txt` to take a dump of the log. You can also view the logs from a PC over `ADB`. For more information, check official android `logcat` guide [here](https://developer.android.com/studio/command-line/logcat).
Moreover, users can generate termux files `stat` info and `logcat` dump automatically too with terminal's long hold options menu `More` -> `Report Issue` option and selecting `YES` in the prompt shown to add debug info. This can be helpful for reporting and debugging other issues. If the report generated is too large, then `Save To File` option in context menu (3 dots on top right) of `ReportActivity` can be used and the file viewed/shared instead.
Users must post complete report (optionally without sensitive info) when reporting issues. Issues opened with **(partial) screenshots of error reports** instead of text will likely be automatically closed/deleted.
##### Log Levels
- `Off` - Log nothing.
- `Normal` - Start logging error, warn and info messages and stacktraces.
- `Debug` - Start logging debug messages.
- `Verbose` - Start logging verbose messages.
##
## For Maintainers and Contributors
The [termux-shared](termux-shared) library was added in [`v0.109`](https://github.com/termux/termux-app/releases/tag/v0.109). It defines shared constants and utils of the Termux app and its plugins. It was created to allow for the removal of all hardcoded paths in the Termux app. Some of the termux plugins are using this as well and rest will in future. If you are contributing code that is using a constant or a util that may be shared, then define it in `termux-shared` library if it currently doesn't exist and reference it from there. Update the relevant changelogs as well. Pull requests using hardcoded values **will/should not** be accepted. Termux app and plugin specific classes must be added under `com.termux.shared.termux` package and general classes outside it. The [`termux-shared` `LICENSE`](termux-shared/LICENSE.md) must also be checked and updated if necessary when contributing code. The licenses of any external library or code must be honoured.
The [termux-shared](termux-shared) library was added in [`v0.109`](https://github.com/termux/termux-app/releases/tag/v0.109). It defines shared constants and utils of Termux app and its plugins. It was created to allow for removal of all hardcoded paths in Termux app. The termux plugins will hopefully use this in future as well. If you are contributing code that is using a constant or a util that may be shared, then define it in `termux-shared` library if it currently doesn't exist and reference it from there. Update the relevant changelogs as well. Pull requests using hardcoded values **will/should not** be accepted.
The main Termux constants are defined by [`TermuxConstants`](https://github.com/termux/termux-app/blob/master/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java) class. It also contains information on how to fork Termux or build it with your own package name. Changing the package name will require building the bootstrap zip packages and other packages with the new `$PREFIX`, check [Building Packages](https://github.com/termux/termux-packages/wiki/Building-packages) for more info.
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.**
- **Added** for new features.
- **Changed** for changes in existing functionality.
- **Deprecated** for soon-to-be removed features.
- **Removed** for now removed features.
- **Fixed** for any bug fixes.
- **Security** in case of vulnerabilities.
##
## 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).
- 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))*
&nbsp;
[<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))*
&nbsp;
[<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))*
&nbsp;
[<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))*
&nbsp;
[<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)

View File

@ -1 +0,0 @@
Check https://termux.dev/security for info on Termux security policies and how to report vulnerabilities.

View File

@ -1,55 +1,44 @@
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"
id "com.android.application"
}
android {
namespace "com.termux"
flavorDimensions "version"
productFlavors {
current {
dimension "version"
targetSdkVersion project.properties.targetSdkVersion.toInteger()
}
updated {
dimension "version"
targetSdkVersion project.properties.updatedTargetSdkVersion.toInteger()
}
}
compileSdkVersion project.properties.compileSdkVersion.toInteger()
ndkVersion = System.getenv("JITPACK_NDK_VERSION") ?: project.properties.ndkVersion
def appVersionName = System.getenv("TERMUX_APP_VERSION_NAME") ?: ""
def apkVersionTag = System.getenv("TERMUX_APK_VERSION_TAG") ?: ""
def splitAPKsForDebugBuilds = System.getenv("TERMUX_SPLIT_APKS_FOR_DEBUG_BUILDS") ?: "1"
def splitAPKsForReleaseBuilds = System.getenv("TERMUX_SPLIT_APKS_FOR_RELEASE_BUILDS") ?: "0" // F-Droid does not support split APKs #1904
ndkVersion project.properties.ndkVersion
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.2.0"
implementation "androidx.core:core:1.5.0-rc01"
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
implementation "androidx.preference:preference:1.1.1"
implementation "androidx.viewpager:viewpager:1.0.0"
implementation "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"
if (appVersionName) versionName = appVersionName
validateVersionName(versionName)
buildConfigField "String", "TERMUX_PACKAGE_VARIANT", "\"" + project.ext.packageVariant + "\"" // Used by TermuxApplication class
versionCode 112
versionName "0.112"
manifestPlaceholders.TERMUX_PACKAGE_NAME = "com.termux"
manifestPlaceholders.TERMUX_APP_NAME = "Termux"
@ -66,20 +55,15 @@ android {
}
}
splits {
abi {
enable ((gradle.startParameter.taskNames.any { it.contains("Debug") } && splitAPKsForDebugBuilds == "1") ||
(gradle.startParameter.taskNames.any { it.contains("Release") } && splitAPKsForReleaseBuilds == "1"))
reset ()
include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
universalApk true
}
ndk {
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
}
}
signingConfigs {
debug {
storeFile file('testkey_untrusted.jks')
storeFile file('dev_keystore.jks')
keyAlias 'alias'
storePassword 'xrj45yWGLbsO7W0v'
keyPassword 'xrj45yWGLbsO7W0v'
@ -89,7 +73,7 @@ android {
buildTypes {
release {
minifyEnabled true
shrinkResources false // Reproducible builds
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
@ -99,9 +83,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 +93,7 @@ android {
}
}
lint {
lintOptions {
disable 'ProtectedPermissions'
}
@ -121,54 +102,75 @@ android {
includeAndroidResources = true
}
}
packagingOptions {
jniLibs {
useLegacyPackaging true
}
}
applicationVariants.all { variant ->
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")
} 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")
}
}
}
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 {
doLast {
print android.defaultConfig.versionName
doLast {
print android.defaultConfig.versionName
}
}
def expandBootstrap(File bootstrapZip, String expectedChecksum, String arch) {
def doneMarkerFile = new File(bootstrapZip.getAbsolutePath() + "." + expectedChecksum + ".done")
if (doneMarkerFile.exists()) return
def archDirName
if (arch == "aarch64") archDirName = "arm64-v8a";
if (arch == "arm") archDirName = "armeabi-v7a";
if (arch == "i686") archDirName = "x86";
if (arch == "x86_64") archDirName = "x86_64";
def outputPath = project.getRootDir().getAbsolutePath() + "/app/src/main/jniLibs/" + archDirName + "/"
def outputDir = new File(outputPath).getAbsoluteFile()
if (!outputDir.exists()) outputDir.mkdirs()
def symlinksFile = new File(outputDir, "libsymlinks.so").getAbsoluteFile()
if (symlinksFile.exists()) symlinksFile.delete();
def mappingsFile = new File(outputDir, "libfiles.so").getAbsoluteFile()
if (mappingsFile.exists()) mappingsFile.delete()
mappingsFile.createNewFile()
def mappingsFileWriter = new BufferedWriter(new FileWriter(mappingsFile))
def counter = 100
new java.util.zip.ZipInputStream(new FileInputStream(bootstrapZip)).withCloseable { zipInput ->
def zipEntry
while ((zipEntry = zipInput.getNextEntry()) != null) {
if (zipEntry.getName() == "SYMLINKS.txt") {
zipInput.transferTo(new FileOutputStream(symlinksFile))
} else if (!zipEntry.isDirectory()) {
def soName = "lib" + counter + ".so"
def targetFile = new File(outputDir, soName).getAbsoluteFile()
println "target file path is ${targetFile}"
try {
zipInput.transferTo(new FileOutputStream(targetFile))
} catch (Exception e) {
println "Error ${e}"
}
mappingsFileWriter.writeLine(soName + "←" + zipEntry.getName())
counter++
}
}
}
mappingsFileWriter.close()
doneMarkerFile.createNewFile()
}
def validateVersionName(String versionName) {
// https://semver.org/spec/v2.0.0.html#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
// ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$
if (!java.util.regex.Pattern.matches("^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?\$", versionName))
throw new GradleException("The versionName '" + versionName + "' is not a valid version as per semantic version '2.0.0' spec in the format 'major.minor.patch(-prerelease)(+buildmetadata)'. https://semver.org/spec/v2.0.0.html.")
}
def downloadBootstrap(String arch, String expectedChecksum, String version) {
def downloadBootstrap(String arch, String expectedChecksum, String version, boolean isPackagesInApk) {
def digest = java.security.MessageDigest.getInstance("SHA-256")
def localUrl = "src/main/cpp/bootstrap-" + arch + ".zip"
def file = new File(projectDir, localUrl)
def file = isPackagesInApk ? new File(project.buildDir, "./gradle/bootstrap-" + arch + "-" + version + ".zip")
: new File(projectDir, "src/main/cpp/bootstrap-" + arch + ".zip");
if (file.exists()) {
def buffer = new byte[8192]
def input = new FileInputStream(file)
@ -178,11 +180,11 @@ def downloadBootstrap(String arch, String expectedChecksum, String version) {
digest.update(buffer, 0, readBytes)
}
def checksum = new BigInteger(1, digest.digest()).toString(16)
while (checksum.length() < 64) { checksum = "0" + checksum }
if (checksum == expectedChecksum) {
if (isPackagesInApk) expandBootstrap(file, expectedChecksum, arch)
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: " + file)
file.delete()
}
}
@ -200,11 +202,11 @@ def downloadBootstrap(String arch, String expectedChecksum, String version) {
out.close()
def checksum = new BigInteger(1, digest.digest()).toString(16)
while (checksum.length() < 64) { checksum = "0" + checksum }
if (checksum != expectedChecksum) {
file.delete()
throw new GradleException("Wrong checksum for " + remoteUrl + ": expected: " + expectedChecksum + ", actual: " + checksum)
}
if (isPackagesInApk) expandBootstrap(file, expectedChecksum, arch)
}
clean {
@ -216,28 +218,18 @@ clean {
}
task downloadBootstraps() {
boolean isPackagesInApk = getGradle().getStartParameter().getTaskRequests().toString().contains("Updated");
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 = "2021.04.13-r1"
downloadBootstrap("aarch64", "ff82e5755d947cd1f3e0b30916d125c6ddd8ba3254801ca7499d73653417e158", version, isPackagesInApk)
downloadBootstrap("arm", "53a7df2d6d0a36a8c9ab5259c8b5457c93b8bae8aec2321a470236b6da54e59a", version, isPackagesInApk)
downloadBootstrap("i686", "f0e1399a13ebed6c5229fde161f9848d9f5eeae7b8cd82f31250a813b52e371", version, isPackagesInApk)
downloadBootstrap("x86_64", "e36c4d8c933dc12b3f48937b7747c7a4dcfaa70f0dd89ad5e8b4465930075ae9", version, isPackagesInApk)
}
}
afterEvaluate {
android.applicationVariants.all { variant ->
variant.javaCompileProvider.get().dependsOn(downloadBootstraps)
}
android.applicationVariants.all { variant ->
variant.javaCompileProvider.get().dependsOn(downloadBootstraps)
}
}

View File

@ -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,9 +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" />
<application
android:name=".app.TermuxApplication"
@ -44,21 +40,24 @@
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">
<!--
This (or rather, value 2.1 or higher) is needed to make the Samsung Galaxy S8
mark the app with "This app is optimized to run in full screen."
-->
<meta-data
android:name="android.max_aspect"
android:value="10.0" />
<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 +76,6 @@
<activity-alias
android:name=".HomeActivity"
android:exported="true"
android:targetActivity=".app.TermuxActivity">
<!-- Launch activity automatically on boot on Android Things devices -->
@ -95,33 +93,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:name=".app.activities.ReportActivity"
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 +129,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 +141,7 @@
<data android:mimeType="text/*" />
<data android:mimeType="video/*" />
</intent-filter>
</activity-alias>
</activity>
<provider
android:name=".filepicker.TermuxDocumentsProvider"
@ -169,35 +154,9 @@
</intent-filter>
</provider>
<provider
android:name=".app.TermuxOpenReceiver$ContentProvider"
android:authorities="${TERMUX_PACKAGE_NAME}.files"
android:exported="true"
android:grantUriPermissions="true"
android:permission="${TERMUX_PACKAGE_NAME}.permission.RUN_COMMAND" />
<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" />
<service
android:name=".app.TermuxService"
android:exported="false" />
<service
android:name=".app.RunCommandService"
android:exported="true"
@ -207,30 +166,21 @@
</intent-filter>
</service>
<receiver android:name=".app.TermuxOpenReceiver" />
<!-- This (or rather, value 2.1 or higher) is needed to make the Samsung Galaxy S8 mark the
app with "This app is optimized to run in full screen." -->
<meta-data
android:name="android.max_aspect"
android:value="10.0" />
<!-- https://developer.samsung.com/samsung-dex/modify-optimizing.html -->
<!-- Version < 3.0. DeX Mode and Screen Mirroring support -->
<meta-data
android:name="com.samsung.android.keepalive.density"
android:value="true" />
<!-- Version >= 3.0. DeX Dual Mode support -->
<meta-data
android:name="com.samsung.android.multidisplay.keep_process_alive"
android:value="true" />
<provider
android:name=".app.TermuxOpenReceiver$ContentProvider"
android:authorities="${TERMUX_PACKAGE_NAME}.files"
android:exported="true"
android:grantUriPermissions="true"
android:readPermission="android.permission.permRead" />
<meta-data
android:name="com.sec.android.support.multiwindow"
android:value="true" />
<meta-data
android:name="com.samsung.android.multidisplay.keep_process_alive"
android:value="true" />
</application>
</manifest>

View File

@ -10,21 +10,15 @@ import android.os.Build;
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.filesystem.FileType;
import com.termux.shared.errors.Errno;
import com.termux.shared.errors.Error;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
import com.termux.shared.file.FileUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.notification.NotificationUtils;
import com.termux.shared.shell.command.ExecutionCommand;
import com.termux.shared.shell.command.ExecutionCommand.Runner;
import com.termux.app.utils.PluginUtils;
import com.termux.shared.data.DataUtils;
import com.termux.shared.models.ExecutionCommand;
/**
* A service that receives {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent from third party apps and
@ -63,85 +57,43 @@ 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);
Error error;
String errmsg;
// If invalid action passed, then just return
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);
return stopService();
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return Service.START_NOT_STICKY;
}
String executableExtra = executionCommand.executable = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH, null);
executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS, null);
/*
* If intent was sent with `am` command, then normal comma characters may have been replaced
* with alternate characters if a normal comma existed in an argument itself to prevent it
* splitting into multiple arguments by `am` command.
* If `tudo` or `sudo` are used, then simply using their `-r` and `--comma-alternative` command
* options can be used without passing the below extras, but native supports is helpful if
* they are not being used.
* https://github.com/agnostic-apollo/tudo#passing-arguments-using-run_command-intent
* https://android.googlesource.com/platform/frameworks/base/+/21bdaf1/cmds/am/src/com/android/commands/am/Am.java#572
*/
boolean replaceCommaAlternativeCharsInArguments = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS, false);
if (replaceCommaAlternativeCharsInArguments) {
String commaAlternativeCharsInArguments = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS, null);
if (commaAlternativeCharsInArguments == null)
commaAlternativeCharsInArguments = TermuxConstants.COMMA_ALTERNATIVE;
// Replace any commaAlternativeCharsInArguments characters with normal commas
DataUtils.replaceSubStringsInStringArrayItems(executionCommand.arguments, commaAlternativeCharsInArguments, TermuxConstants.COMMA_NORMAL);
}
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.backgroundCustomLogLevel = IntentUtils.getIntegerExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL, null);
executionCommand.executable = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
executionCommand.arguments = intent.getStringArrayExtra(RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS);
executionCommand.stdin = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_STDIN);
executionCommand.workingDirectory = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_WORKDIR);
executionCommand.inBackground = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false);
executionCommand.sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION);
executionCommand.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);
executionCommand.commandLabel = DataUtils.getDefaultIfNull(intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL), "RUN_COMMAND Execution Intent Command");
executionCommand.commandDescription = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION);
executionCommand.commandHelp = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_HELP);
executionCommand.isPluginExecutionCommand = true;
executionCommand.resultConfig.resultPendingIntent = intent.getParcelableExtra(RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT);
executionCommand.resultConfig.resultDirectoryPath = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_DIRECTORY, null);
if (executionCommand.resultConfig.resultDirectoryPath != null) {
executionCommand.resultConfig.resultSingleFile = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_RESULT_SINGLE_FILE, false);
executionCommand.resultConfig.resultFileBasename = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_BASENAME, null);
executionCommand.resultConfig.resultFileOutputFormat = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, null);
executionCommand.resultConfig.resultFileErrorFormat = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, null);
executionCommand.resultConfig.resultFilesSuffix = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILES_SUFFIX, null);
}
executionCommand.pluginPendingIntent = intent.getParcelableExtra(RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT);
// If "allow-external-apps" property to not set to "true", then just return
// We enable force notifications if "allow-external-apps" policy is violated so that the
// user knows someone tried to run a command in termux context, since it may be malicious
// app or imported (tasker) plugin project and not the user himself. If a pending intent is
// also sent, then its creator is also logged and shown.
errmsg = TermuxPluginUtils.checkIfAllowExternalAppsPolicyIsViolated(this, LOG_TAG);
errmsg = PluginUtils.checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(this);
if (errmsg != null) {
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true);
return stopService();
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true);
return Service.START_NOT_STICKY;
}
@ -149,23 +101,24 @@ public class RunCommandService extends Service {
// If executable is null or empty, then exit here instead of getting canonical path which would expand to "/"
if (executionCommand.executable == null || executionCommand.executable.isEmpty()) {
errmsg = this.getString(R.string.error_run_command_service_mandatory_extra_missing, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return stopService();
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return Service.START_NOT_STICKY;
}
// Get canonical path of executable
executionCommand.executable = TermuxFileUtils.getCanonicalPath(executionCommand.executable, null, true);
executionCommand.executable = FileUtils.getCanonicalPath(executionCommand.executable, null, true);
// If executable is not a regular file, or is not readable or executable, then just return
// Setting of missing read and execute permissions is not done
error = FileUtils.validateRegularFileExistenceAndPermissions("executable", executionCommand.executable, null,
FileUtils.APP_EXECUTABLE_FILE_PERMISSIONS, true, true,
errmsg = FileUtils.validateRegularFileExistenceAndPermissions(this, "executable", executionCommand.executable, null,
PluginUtils.PLUGIN_EXECUTABLE_FILE_PERMISSIONS, true, true,
false);
if (error != null) {
executionCommand.setStateFailed(error);
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return stopService();
if (errmsg != null) {
errmsg += "\n" + this.getString(R.string.msg_executable_absolute_path, executionCommand.executable);
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return Service.START_NOT_STICKY;
}
@ -173,36 +126,29 @@ public class RunCommandService extends Service {
// If workingDirectory is not null or empty
if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) {
// Get canonical path of workingDirectory
executionCommand.workingDirectory = TermuxFileUtils.getCanonicalPath(executionCommand.workingDirectory, null, true);
executionCommand.workingDirectory = FileUtils.getCanonicalPath(executionCommand.workingDirectory, null, true);
// If workingDirectory is not a directory, or is not readable or writable, then just return
// Creation of missing directory and setting of read, write and execute permissions are only done if workingDirectory is
// under allowed termux working directory paths.
// under {@link TermuxConstants#TERMUX_FILES_DIR_PATH}
// We try to set execute permissions, but ignore if they are missing, since only read and write permissions are required
// for working directories.
error = TermuxFileUtils.validateDirectoryFileExistenceAndPermissions("working", executionCommand.workingDirectory,
true, true, true,
false, true);
if (error != null) {
executionCommand.setStateFailed(error);
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return stopService();
errmsg = FileUtils.validateDirectoryFileExistenceAndPermissions(this, "working", executionCommand.workingDirectory, TermuxConstants.TERMUX_FILES_DIR_PATH, true,
PluginUtils.PLUGIN_WORKING_DIRECTORY_PERMISSIONS, true, true,
true, true);
if (errmsg != null) {
errmsg += "\n" + this.getString(R.string.msg_working_directory_absolute_path, executionCommand.workingDirectory);
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return Service.START_NOT_STICKY;
}
}
// If the executable passed as the extra was an applet for coreutils/busybox, then we must
// use it instead of the canonical path above since otherwise arguments would be passed to
// coreutils/busybox instead and command would fail. Broken symlinks would already have been
// validated so it should be fine to use it.
executableExtra = TermuxFileUtils.getExpandedTermuxPath(executableExtra);
if (FileUtils.getFileType(executableExtra, false) == FileType.SYMLINK) {
Logger.logVerbose(LOG_TAG, "The executableExtra path \"" + executableExtra + "\" is a symlink so using it instead of the canonical path \"" + executionCommand.executable + "\"");
executionCommand.executable = executableExtra;
}
executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(executionCommand.executable).build();
Logger.logVerboseExtended(LOG_TAG, executionCommand.toString());
executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(FileUtils.getExpandedTermuxPath(executionCommand.executable)).build();
Logger.logVerbose(LOG_TAG, executionCommand.toString());
// Create execution intent with the action TERMUX_SERVICE#ACTION_SERVICE_EXECUTE to be sent to the TERMUX_SERVICE
Intent execIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, executionCommand.executableUri);
@ -210,24 +156,13 @@ 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_CUSTOM_LOG_LEVEL, DataUtils.getStringFromInteger(executionCommand.backgroundCustomLogLevel, null));
execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, executionCommand.inBackground);
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);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP, executionCommand.pluginAPIHelp);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT, executionCommand.resultConfig.resultPendingIntent);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_DIRECTORY, executionCommand.resultConfig.resultDirectoryPath);
if (executionCommand.resultConfig.resultDirectoryPath != null) {
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_SINGLE_FILE, executionCommand.resultConfig.resultSingleFile);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_BASENAME, executionCommand.resultConfig.resultFileBasename);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, executionCommand.resultConfig.resultFileOutputFormat);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, executionCommand.resultConfig.resultFileErrorFormat);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILES_SUFFIX, executionCommand.resultConfig.resultFilesSuffix);
}
execIntent.putExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT, executionCommand.pluginPendingIntent);
// Start TERMUX_SERVICE and pass it execution intent
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -236,11 +171,8 @@ public class RunCommandService extends Service {
this.startService(execIntent);
}
return stopService();
}
private int stopService() {
runStopForeground();
return Service.START_NOT_STICKY;
}
@ -262,7 +194,7 @@ public class RunCommandService extends Service {
Notification.Builder builder = NotificationUtils.geNotificationBuilder(this,
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_LOW,
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_NAME, null, null,
null, null, NotificationUtils.NOTIFICATION_MODE_SILENT);
null, NotificationUtils.NOTIFICATION_MODE_SILENT);
if (builder == null) return null;
// No need to show a timestamp:

File diff suppressed because it is too large Load Diff

View File

@ -1,85 +1,28 @@
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.CrashHandler;
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);
CrashHandler.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);
if (preferences == null) return;
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(getApplicationContext());
preferences.setLogLevel(null, preferences.getLogLevel());
Logger.logDebug("Starting Application");
}
}

View File

@ -4,24 +4,16 @@ import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.Build;
import android.os.Environment;
import android.os.UserManager;
import android.system.Os;
import android.util.Pair;
import android.view.WindowManager;
import com.termux.R;
import com.termux.shared.file.FileUtils;
import com.termux.shared.termux.crash.TermuxCrashUtils;
import com.termux.shared.termux.file.TermuxFileUtils;
import com.termux.shared.interact.MessageDialogUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.errors.Error;
import com.termux.shared.android.PackageUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxUtils;
import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
@ -33,11 +25,6 @@ import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import static com.termux.shared.termux.TermuxConstants.TERMUX_PREFIX_DIR;
import static com.termux.shared.termux.TermuxConstants.TERMUX_PREFIX_DIR_PATH;
import static com.termux.shared.termux.TermuxConstants.TERMUX_STAGING_PREFIX_DIR;
import static com.termux.shared.termux.TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH;
/**
* Install the Termux bootstrap packages if necessary by following the below steps:
* <p/>
@ -63,55 +50,33 @@ final class TermuxInstaller {
/** Performs bootstrap setup if necessary. */
static void setupBootstrapIfNeeded(final Activity activity, final Runnable whenDone) {
String bootstrapErrorMessage;
Error filesDirectoryAccessibleError;
// This will also call Context.getFilesDir(), which should ensure that termux files directory
// is created if it does not already exist
filesDirectoryAccessibleError = TermuxFileUtils.isTermuxFilesDirectoryAccessible(activity, true, true);
boolean isFilesDirectoryAccessible = filesDirectoryAccessibleError == null;
// 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));
Logger.logError(LOG_TAG, "isFilesDirectoryAccessible: " + isFilesDirectoryAccessible);
UserManager um = (UserManager) activity.getSystemService(Context.USER_SERVICE);
boolean isPrimaryUser = um.getSerialNumberForUser(android.os.Process.myUserHandle()) == 0;
if (!isPrimaryUser) {
String bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message, TermuxConstants.TERMUX_PREFIX_DIR_PATH);
Logger.logError(LOG_TAG, bootstrapErrorMessage);
sendBootstrapCrashReportNotification(activity, bootstrapErrorMessage);
MessageDialogUtils.exitAppWithErrorMessage(activity,
activity.getString(R.string.bootstrap_error_title),
bootstrapErrorMessage);
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(bootstrapErrorMessage)
.setOnDismissListener(dialog -> System.exit(0)).setPositiveButton(android.R.string.ok, null).show();
return;
}
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));
}
Logger.logError(LOG_TAG, bootstrapErrorMessage);
sendBootstrapCrashReportNotification(activity, bootstrapErrorMessage);
MessageDialogUtils.showMessage(activity,
activity.getString(R.string.bootstrap_error_title),
bootstrapErrorMessage, null);
return;
}
final String PREFIX_FILE_PATH = TermuxConstants.TERMUX_PREFIX_DIR_PATH;
final File PREFIX_FILE = TermuxConstants.TERMUX_PREFIX_DIR;
// 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.");
} else {
whenDone.run();
return;
}
} else if (FileUtils.fileExists(TERMUX_PREFIX_DIR_PATH, false)) {
Logger.logInfo(LOG_TAG, "The termux prefix directory \"" + TERMUX_PREFIX_DIR_PATH + "\" does not exist but another file exists at its destination.");
if (FileUtils.directoryFileExists(PREFIX_FILE_PATH, true)) {
File[] PREFIX_FILE_LIST = PREFIX_FILE.listFiles();
// If prefix directory is empty or only contains the tmp directory
if(PREFIX_FILE_LIST == null || PREFIX_FILE_LIST.length == 0 || (PREFIX_FILE_LIST.length == 1 && TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH.equals(PREFIX_FILE_LIST[0].getAbsolutePath()))) {
Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" exists but is empty or only contains the tmp directory.");
} else {
whenDone.run();
return;
}
} else if (FileUtils.fileExists(PREFIX_FILE_PATH, false)) {
Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" does not exist but another file exists at its destination.");
}
final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false);
@ -121,37 +86,24 @@ final class TermuxInstaller {
try {
Logger.logInfo(LOG_TAG, "Installing " + TermuxConstants.TERMUX_APP_NAME + " bootstrap packages.");
Error error;
String errmsg;
final String STAGING_PREFIX_PATH = TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH;
final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH);
// Delete prefix staging directory or any file at its destination
error = FileUtils.deleteFile("termux prefix staging directory", TERMUX_STAGING_PREFIX_DIR_PATH, true);
if (error != null) {
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
return;
errmsg = FileUtils.deleteFile(activity, "prefix staging directory", STAGING_PREFIX_PATH, true);
if (errmsg != null) {
throw new RuntimeException(errmsg);
}
// Delete prefix directory or any file at its destination
error = FileUtils.deleteFile("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true);
if (error != null) {
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
return;
errmsg = FileUtils.deleteFile(activity, "prefix directory", PREFIX_FILE_PATH, true);
if (errmsg != null) {
throw new RuntimeException(errmsg);
}
// Create prefix staging directory if it does not already exist and set required permissions
error = TermuxFileUtils.isTermuxPrefixStagingDirectoryAccessible(true, true);
if (error != null) {
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
return;
}
// Create prefix directory if it does not already exist and set required permissions
error = TermuxFileUtils.isTermuxPrefixDirectoryAccessible(true, true);
if (error != null) {
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
return;
}
Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + TERMUX_STAGING_PREFIX_DIR_PATH + "\".");
Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + STAGING_PREFIX_PATH + "\".");
final byte[] buffer = new byte[8096];
final List<Pair<String, String>> symlinks = new ArrayList<>(50);
@ -168,25 +120,17 @@ final class TermuxInstaller {
if (parts.length != 2)
throw new RuntimeException("Malformed symlink line: " + line);
String oldPath = parts[0];
String newPath = TERMUX_STAGING_PREFIX_DIR_PATH + "/" + parts[1];
String newPath = STAGING_PREFIX_PATH + "/" + parts[1];
symlinks.add(Pair.create(oldPath, newPath));
error = ensureDirectoryExists(new File(newPath).getParentFile());
if (error != null) {
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
return;
}
ensureDirectoryExists(activity, new File(newPath).getParentFile());
}
} else {
String zipEntryName = zipEntry.getName();
File targetFile = new File(TERMUX_STAGING_PREFIX_DIR_PATH, zipEntryName);
File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName);
boolean isDirectory = zipEntry.isDirectory();
error = ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile());
if (error != null) {
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
return;
}
ensureDirectoryExists(activity, isDirectory ? targetFile : targetFile.getParentFile());
if (!isDirectory) {
try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
@ -194,8 +138,7 @@ final class TermuxInstaller {
while ((readBytes = zipInput.read(buffer)) != -1)
outStream.write(buffer, 0, readBytes);
}
if (zipEntryName.startsWith("bin/") || zipEntryName.startsWith("libexec") ||
zipEntryName.startsWith("lib/apt/apt-helper") || zipEntryName.startsWith("lib/apt/methods")) {
if (zipEntryName.startsWith("bin/") || zipEntryName.startsWith("libexec") || zipEntryName.startsWith("lib/apt/methods")) {
//noinspection OctalInteger
Os.chmod(targetFile.getAbsolutePath(), 0700);
}
@ -210,22 +153,30 @@ final class TermuxInstaller {
Os.symlink(symlink.first, symlink.second);
}
Logger.logInfo(LOG_TAG, "Moving termux prefix staging to prefix directory.");
Logger.logInfo(LOG_TAG, "Moving prefix staging to prefix directory.");
if (!TERMUX_STAGING_PREFIX_DIR.renameTo(TERMUX_PREFIX_DIR)) {
throw new RuntimeException("Moving termux prefix staging to prefix directory failed");
if (!STAGING_PREFIX_FILE.renameTo(PREFIX_FILE)) {
throw new RuntimeException("Moving prefix staging to prefix directory failed");
}
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) {
showBootstrapErrorDialog(activity, whenDone, Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)));
Logger.logStackTraceWithMessage(LOG_TAG, "Bootstrap error", e);
activity.runOnUiThread(() -> {
try {
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
.setNegativeButton(R.string.bootstrap_error_abort, (dialog, which) -> {
dialog.dismiss();
activity.finish();
}).setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
dialog.dismiss();
TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone);
}).show();
} catch (WindowManager.BadTokenException e1) {
// Activity already dismissed - ignore.
}
});
} finally {
activity.runOnUiThread(() -> {
try {
@ -239,72 +190,28 @@ final class TermuxInstaller {
}.start();
}
public static void showBootstrapErrorDialog(Activity activity, Runnable whenDone, String message) {
Logger.logErrorExtended(LOG_TAG, "Bootstrap Error:\n" + message);
// Send a notification with the exception so that the user knows why bootstrap setup failed
sendBootstrapCrashReportNotification(activity, message);
activity.runOnUiThread(() -> {
try {
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
.setNegativeButton(R.string.bootstrap_error_abort, (dialog, which) -> {
dialog.dismiss();
activity.finish();
})
.setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
dialog.dismiss();
FileUtils.deleteFile("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true);
TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone);
}).show();
} catch (WindowManager.BadTokenException e1) {
// Activity already dismissed - ignore.
}
});
}
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" +
TermuxUtils.getTermuxDebugMarkdownString(activity),
true, false, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGES, true);
}
static void setupStorageSymlinks(final Context context) {
final String LOG_TAG = "termux-storage";
final String title = TermuxConstants.TERMUX_APP_NAME + " Setup Storage Error";
Logger.logInfo(LOG_TAG, "Setting up storage symlinks.");
new Thread() {
public void run() {
try {
Error error;
String errmsg;
File storageDir = TermuxConstants.TERMUX_STORAGE_HOME_DIR;
error = FileUtils.clearDirectory("~/storage", storageDir.getAbsolutePath());
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);
errmsg = FileUtils.clearDirectory(context, "~/storage", storageDir.getAbsolutePath());
if (errmsg != null) {
Logger.logErrorAndShowToast(context, LOG_TAG, errmsg);
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 +227,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,32 +238,21 @@ 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);
Logger.logStackTraceWithMessage(LOG_TAG, "Error setting up link", e);
}
}
}.start();
}
private static Error ensureDirectoryExists(File directory) {
return FileUtils.createDirectoryFile(directory.getAbsolutePath());
public static void ensureDirectoryExists(Context context, File directory) {
String errmsg;
errmsg = FileUtils.createDirectoryFile(context, directory.getAbsolutePath());
if (errmsg != null) {
throw new RuntimeException(errmsg);
}
}
public static byte[] loadZipBytes() {

View File

@ -13,12 +13,7 @@ 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.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 +30,11 @@ 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 +48,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 +61,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 +89,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,14 +105,12 @@ 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);
}
}
public static class ContentProvider extends android.content.ContentProvider {
private static final String LOG_TAG = "TermuxContentProvider";
@Override
public boolean onCreate() {
return true;
@ -172,13 +155,6 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
@Override
public String getType(@NonNull Uri uri) {
String path = uri.getLastPathSegment();
int extIndex = path.lastIndexOf('.') + 1;
if (extIndex > 0) {
MimeTypeMap mimeMap = MimeTypeMap.getSingleton();
String ext = path.substring(extIndex).toLowerCase();
return mimeMap.getMimeTypeFromExtension(ext);
}
return null;
}
@ -202,33 +178,15 @@ 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 + "\"");
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 (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)) {
mode = "r";
}
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode(mode));
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
}

View File

@ -5,46 +5,37 @@ import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
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 androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.provider.Settings;
import android.widget.ArrayAdapter;
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.shared.data.IntentUtils;
import com.termux.shared.net.uri.UriUtils;
import com.termux.shared.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.app.terminal.TermuxTerminalSessionClient;
import com.termux.app.utils.PluginUtils;
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.shell.ShellUtils;
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;
@ -52,9 +43,11 @@ import com.termux.terminal.TerminalSessionClient;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
/**
* 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 +58,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 +71,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;
@ -110,16 +112,13 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
@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);
if (getApplicationContext().getApplicationInfo().targetSdkVersion >= 30) {
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.termux","com.termux.app.TermuxPackageInstallerService"));
startService(intent);
}
}
@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) {
@ -169,16 +164,11 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
public void onDestroy() {
Logger.logVerbose(LOG_TAG, "onDestroy");
TermuxShellUtils.clearTermuxTMPDIR(true);
ShellUtils.clearTermuxTMPDIR(this, true);
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();
processResult = mWantsToStop || (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null);
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())
if (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null)
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);
if (!executionCommand.shouldNotProcessResults() && executionCommand.pluginPendingIntent != null) {
if (executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_CANCELED, this.getString(com.termux.shared.R.string.error_execution_cancelled), null)) {
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,65 +354,35 @@ 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.arguments = IntentUtils.getStringArrayExtraIfSet(intent, TERMUX_SERVICE.EXTRA_ARGUMENTS, null);
if (Runner.APP_SHELL.equalsRunner(executionCommand.runner))
executionCommand.stdin = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_STDIN, null);
executionCommand.backgroundCustomLogLevel = IntentUtils.getIntegerExtraIfSet(intent, TERMUX_SERVICE.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL, null);
executionCommand.executable = executionCommand.executableUri.getPath();
executionCommand.arguments = intent.getStringArrayExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS);
if (executionCommand.inBackground)
executionCommand.stdin = intent.getStringExtra(TERMUX_SERVICE.EXTRA_STDIN);
}
executionCommand.workingDirectory = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_WORKDIR, null);
executionCommand.isFailsafe = intent.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false);
executionCommand.workingDirectory = intent.getStringExtra(TERMUX_SERVICE.EXTRA_WORKDIR);
executionCommand.isFailsafe = intent.getBooleanExtra(TERMUX_ACTIVITY.ACTION_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.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) {
executionCommand.resultConfig.resultSingleFile = intent.getBooleanExtra(TERMUX_SERVICE.EXTRA_RESULT_SINGLE_FILE, false);
executionCommand.resultConfig.resultFileBasename = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILE_BASENAME, null);
executionCommand.resultConfig.resultFileOutputFormat = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, null);
executionCommand.resultConfig.resultFileErrorFormat = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, null);
executionCommand.resultConfig.resultFilesSuffix = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILES_SUFFIX, null);
}
if (executionCommand.shellCreateMode == null)
executionCommand.shellCreateMode = ShellCreateMode.ALWAYS.getMode();
executionCommand.commandLabel = DataUtils.getDefaultIfNull(intent.getStringExtra(TERMUX_SERVICE.EXTRA_COMMAND_LABEL), "Execution Intent Command");
executionCommand.commandDescription = intent.getStringExtra(TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION);
executionCommand.commandHelp = intent.getStringExtra(TERMUX_SERVICE.EXTRA_COMMAND_HELP);
executionCommand.pluginAPIHelp = intent.getStringExtra(TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP);
executionCommand.isPluginExecutionCommand = true;
executionCommand.pluginPendingIntent = intent.getParcelableExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT);
// 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 +390,57 @@ 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());
Logger.logVerbose(LOG_TAG, executionCommand.toString());
AppShell newTermuxTask = AppShell.execute(this, executionCommand, this,
new TermuxShellEnvironment(), null,false);
TermuxTask newTermuxTask = TermuxTask.execute(this, executionCommand, this, 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());
}
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 +449,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 +468,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 +485,51 @@ 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());
Logger.logVerbose(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);
TermuxSession newTermuxSession = TermuxSession.execute(this, executionCommand, getTermuxTerminalSessionClient(), this, 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());
}
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 +539,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,14 +554,14 @@ 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();
@ -662,24 +571,6 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
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;
}
}
/** 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 +578,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 +589,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)
@ -717,13 +608,8 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
// For android >= 10, apps require Display over other apps permission to start foreground activities
// from background (services). If it is not granted, then TermuxSessions that are started will
// show in Termux notification but will not run until user manually clicks the notification.
if (PermissionUtils.validateDisplayOverOtherAppsPermissionForPostAndroid10(this, true)) {
if (PermissionUtils.validateDisplayOverOtherAppsPermissionForPostAndroid10(this)) {
TermuxActivity.startTermuxActivity(this);
} 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);
}
}
@ -733,35 +619,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 +655,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;
}
@ -784,12 +670,12 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
// Set pending intent to be launched when notification is clicked
Intent notificationIntent = TermuxActivity.newInstance(this);
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
// 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");
@ -808,8 +694,8 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
// Build the notification
Notification.Builder builder = NotificationUtils.geNotificationBuilder(this,
TermuxConstants.TERMUX_APP_NOTIFICATION_CHANNEL_ID, priority,
TermuxConstants.TERMUX_APP_NAME, notificationText, null,
contentIntent, null, NotificationUtils.NOTIFICATION_MODE_SILENT);
getText(R.string.application_name), notificationText, null,
pendingIntent, NotificationUtils.NOTIFICATION_MODE_SILENT);
if (builder == null) return null;
// No need to show a timestamp:
@ -850,7 +736,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 +748,40 @@ public final class TermuxService extends Service implements AppShell.AppShellCli
private void setCurrentStoredTerminalSession(TerminalSession terminalSession) {
if (terminalSession == null) return;
// Make the newly created session the current one to be displayed
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(this);
if (preferences == null) return;
preferences.setCurrentSession(terminalSession.mHandle);
private void setCurrentStoredTerminalSession(TerminalSession session) {
if (session == null) return;
// Make the newly created session the current one to be displayed:
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(this);
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 +789,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;
}

View File

@ -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,8 @@ 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,13 +32,14 @@ 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);
mWebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.equals(TermuxConstants.TERMUX_WIKI_URL) || url.startsWith(TermuxConstants.TERMUX_WIKI_URL + "/")) {
if (url.startsWith("https://wiki.termux.com")) {
// Inline help.
setContentView(progressLayout);
return false;
@ -62,7 +60,7 @@ public final class HelpActivity extends AppCompatActivity {
setContentView(mWebView);
}
});
mWebView.loadUrl(TermuxConstants.TERMUX_WIKI_URL);
mWebView.loadUrl("https://wiki.termux.com/wiki/Main_Page");
}
@Override

View File

@ -0,0 +1,180 @@
package com.termux.app.activities;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import com.termux.R;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.interact.ShareUtils;
import com.termux.app.models.ReportInfo;
import org.commonmark.node.FencedCodeBlock;
import io.noties.markwon.Markwon;
import io.noties.markwon.recycler.MarkwonAdapter;
import io.noties.markwon.recycler.SimpleEntry;
public class ReportActivity extends AppCompatActivity {
private static final String EXTRA_REPORT_INFO = "report_info";
ReportInfo mReportInfo;
String mReportMarkdownString;
String mReportActivityMarkdownString;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_report);
Toolbar toolbar = findViewById(R.id.toolbar);
if (toolbar != null) {
setSupportActionBar(toolbar);
}
Bundle bundle = null;
Intent intent = getIntent();
if (intent != null)
bundle = intent.getExtras();
else if (savedInstanceState != null)
bundle = savedInstanceState;
updateUI(bundle);
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
if (intent != null)
updateUI(intent.getExtras());
}
private void updateUI(Bundle bundle) {
if (bundle == null) {
finish();
return;
}
mReportInfo = (ReportInfo) bundle.getSerializable(EXTRA_REPORT_INFO);
if (mReportInfo == null) {
finish();
return;
}
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
if (mReportInfo.reportTitle != null)
actionBar.setTitle(mReportInfo.reportTitle);
else
actionBar.setTitle(TermuxConstants.TERMUX_APP_NAME + " App Report");
}
RecyclerView recyclerView = findViewById(R.id.recycler_view);
final Markwon markwon = MarkdownUtils.getRecyclerMarkwonBuilder(this);
final MarkwonAdapter adapter = MarkwonAdapter.builderTextViewIsRoot(R.layout.activity_report_adapter_node_default)
.include(FencedCodeBlock.class, SimpleEntry.create(R.layout.activity_report_adapter_node_code_block, R.id.code_text_view))
.build();
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(adapter);
generateReportActivityMarkdownString();
adapter.setMarkdown(markwon, mReportActivityMarkdownString);
adapter.notifyDataSetChanged();
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable(EXTRA_REPORT_INFO, mReportInfo);
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
final MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_report, menu);
return true;
}
@Override
public void onBackPressed() {
// Remove activity from recents menu on back button press
finishAndRemoveTask();
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
int id = item.getItemId();
if (id == R.id.menu_item_share_report) {
if (mReportMarkdownString != null)
ShareUtils.shareText(this, getString(R.string.title_report_text), mReportMarkdownString);
} else if (id == R.id.menu_item_copy_report) {
if (mReportMarkdownString != null)
ShareUtils.copyTextToClipboard(this, mReportMarkdownString, null);
}
return false;
}
/**
* Generate the markdown {@link String} to be shown in {@link ReportActivity}.
*/
private void generateReportActivityMarkdownString() {
mReportMarkdownString = ReportInfo.getReportInfoMarkdownString(mReportInfo);
mReportActivityMarkdownString = "";
if (mReportInfo.reportStringPrefix != null)
mReportActivityMarkdownString += mReportInfo.reportStringPrefix;
mReportActivityMarkdownString += mReportMarkdownString;
if (mReportInfo.reportStringSuffix != null)
mReportActivityMarkdownString += mReportInfo.reportStringSuffix;
}
public static void startReportActivity(@NonNull final Context context, @NonNull final ReportInfo reportInfo) {
context.startActivity(newInstance(context, reportInfo));
}
public static Intent newInstance(@NonNull final Context context, @NonNull final ReportInfo reportInfo) {
Intent intent = new Intent(context, ReportActivity.class);
Bundle bundle = new Bundle();
bundle.putSerializable(EXTRA_REPORT_INFO, reportInfo);
intent.putExtras(bundle);
// Note that ReportActivity task has documentLaunchMode="intoExisting" set in AndroidManifest.xml
// which has equivalent behaviour to the following. The following dynamic way doesn't seem to
// work for notification pending intent, i.e separate task isn't created and activity is
// launched in the same task as TermuxActivity.
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
return intent;
}
}

View File

@ -1,39 +1,18 @@
package com.termux.app.activities;
import android.content.Context;
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;
import com.termux.R;
import com.termux.shared.activities.ReportActivity;
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.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 +20,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
@ -55,114 +36,7 @@ public class SettingsActivity extends AppCompatActivity {
public static class RootPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
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();
}
private void configureTermuxAPIPreference(@NonNull Context context) {
Preference termuxAPIPreference = findPreference("termux_api");
if (termuxAPIPreference != null) {
TermuxAPIAppSharedPreferences preferences = TermuxAPIAppSharedPreferences.build(context, false);
// If failed to get app preferences, then likely app is not installed, so do not show its preference
termuxAPIPreference.setVisible(preferences != null);
}
}
private void configureTermuxFloatPreference(@NonNull Context context) {
Preference termuxFloatPreference = findPreference("termux_float");
if (termuxFloatPreference != null) {
TermuxFloatAppSharedPreferences preferences = TermuxFloatAppSharedPreferences.build(context, false);
// If failed to get app preferences, then likely app is not installed, so do not show its preference
termuxFloatPreference.setVisible(preferences != null);
}
}
private void configureTermuxTaskerPreference(@NonNull Context context) {
Preference termuxTaskerPreference = findPreference("termux_tasker");
if (termuxTaskerPreference != null) {
TermuxTaskerAppSharedPreferences preferences = TermuxTaskerAppSharedPreferences.build(context, false);
// If failed to get app preferences, then likely app is not installed, so do not show its preference
termuxTaskerPreference.setVisible(preferences != null);
}
}
private void configureTermuxWidgetPreference(@NonNull Context context) {
Preference termuxWidgetPreference = findPreference("termux_widget");
if (termuxWidgetPreference != null) {
TermuxWidgetAppSharedPreferences preferences = TermuxWidgetAppSharedPreferences.build(context, false);
// If failed to get app preferences, then likely app is not installed, so do not show its preference
termuxWidgetPreference.setVisible(preferences != null);
}
}
private void configureAboutPreference(@NonNull Context context) {
Preference aboutPreference = findPreference("about");
if (aboutPreference != null) {
aboutPreference.setOnPreferenceClickListener(preference -> {
new Thread() {
@Override
public void run() {
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("\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,
Environment.getExternalStorageDirectory() + "/" +
FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true));
ReportActivity.startReportActivity(context, reportInfo);
}
}.start();
return true;
});
}
}
private void configureDonatePreference(@NonNull Context context) {
Preference donatePreference = findPreference("donate");
if (donatePreference != null) {
String signingCertificateSHA256Digest = PackageUtils.getSigningCertificateSHA256DigestForPackage(context);
if (signingCertificateSHA256Digest != null) {
// If APK is a Google Playstore release, then do not show the donation link
// since Termux isn't exempted from the playstore policy donation links restriction
// Check Fund solicitations: https://pay.google.com/intl/en_in/about/policy/
String apkRelease = TermuxUtils.getAPKRelease(signingCertificateSHA256Digest);
if (apkRelease == null || apkRelease.equals(TermuxConstants.APK_RELEASE_GOOGLE_PLAYSTORE_SIGNING_CERTIFICATE_SHA256_DIGEST)) {
donatePreference.setVisible(false);
return;
} else {
donatePreference.setVisible(true);
}
}
donatePreference.setOnPreferenceClickListener(preference -> {
ShareUtils.openUrl(context, TermuxConstants.TERMUX_DONATE_URL);
return true;
});
}
}
}

View File

@ -1,287 +0,0 @@
package com.termux.app.api.file;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.provider.OpenableColumns;
import android.util.Patterns;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import com.termux.R;
import com.termux.shared.android.PackageUtils;
import com.termux.shared.data.DataUtils;
import com.termux.shared.data.IntentUtils;
import com.termux.shared.net.uri.UriUtils;
import com.termux.shared.interact.MessageDialogUtils;
import com.termux.shared.net.uri.UriScheme;
import com.termux.shared.termux.interact.TextInputDialogUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
import com.termux.app.TermuxService;
import com.termux.shared.logger.Logger;
import com.termux.shared.termux.settings.properties.TermuxAppSharedProperties;
import com.termux.shared.termux.settings.properties.TermuxPropertyConstants;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.regex.Pattern;
public class FileReceiverActivity extends AppCompatActivity {
static final String TERMUX_RECEIVEDIR = TermuxConstants.TERMUX_FILES_DIR_PATH + "/home/downloads";
static final String EDITOR_PROGRAM = TermuxConstants.TERMUX_HOME_DIR_PATH + "/bin/termux-file-editor";
static final String URL_OPENER_PROGRAM = TermuxConstants.TERMUX_HOME_DIR_PATH + "/bin/termux-url-opener";
/**
* If the activity should be finished when the name input dialog is dismissed. This is disabled
* before showing an error dialog, since the act of showing the error dialog will cause the
* name input dialog to be implicitly dismissed, and we do not want to finish the activity directly
* when showing the error dialog.
*/
boolean mFinishOnDismissNameDialog = true;
private static final String API_TAG = TermuxConstants.TERMUX_APP_NAME + "FileReceiver";
private static final String LOG_TAG = "FileReceiverActivity";
static boolean isSharedTextAnUrl(String sharedText) {
if (sharedText == null || sharedText.isEmpty()) return false;
return Patterns.WEB_URL.matcher(sharedText).matches()
|| Pattern.matches("magnet:\\?xt=urn:btih:.*?", sharedText);
}
@Override
protected void onResume() {
super.onResume();
final Intent intent = getIntent();
final String action = intent.getAction();
final String type = intent.getType();
final String scheme = intent.getScheme();
Logger.logVerbose(LOG_TAG, "Intent Received:\n" + IntentUtils.getIntentString(intent));
final String sharedTitle = IntentUtils.getStringExtraIfSet(intent, Intent.EXTRA_TITLE, null);
if (Intent.ACTION_SEND.equals(action) && type != null) {
final String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
final Uri sharedUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
if (sharedUri != null) {
handleContentUri(sharedUri, sharedTitle);
} else if (sharedText != null) {
if (isSharedTextAnUrl(sharedText)) {
handleUrlAndFinish(sharedText);
} else {
String subject = IntentUtils.getStringExtraIfSet(intent, Intent.EXTRA_SUBJECT, null);
if (subject == null) subject = sharedTitle;
if (subject != null) subject += ".txt";
promptNameAndSave(new ByteArrayInputStream(sharedText.getBytes(StandardCharsets.UTF_8)), subject);
}
} else {
showErrorDialogAndQuit("Send action without content - nothing to save.");
}
} else {
Uri dataUri = intent.getData();
if (dataUri == null) {
showErrorDialogAndQuit("Data uri not passed.");
return;
}
if (UriScheme.SCHEME_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);
if (DataUtils.isNullOrEmpty(path)) {
showErrorDialogAndQuit("File path from data uri is null, empty or invalid.");
return;
}
File file = new File(path);
try {
FileInputStream in = new FileInputStream(file);
promptNameAndSave(in, file.getName());
} catch (FileNotFoundException e) {
showErrorDialogAndQuit("Cannot open file: " + e.getMessage() + ".");
}
} else {
showErrorDialogAndQuit("Unable to receive any file or URL.");
}
}
}
void showErrorDialogAndQuit(String message) {
mFinishOnDismissNameDialog = false;
MessageDialogUtils.showMessage(this,
API_TAG, message,
null, (dialog, which) -> finish(),
null, null,
dialog -> finish());
}
void handleContentUri(@NonNull final Uri uri, String subjectFromIntent) {
try {
Logger.logVerbose(LOG_TAG, "uri: \"" + uri + "\", path: \"" + uri.getPath() + "\", fragment: \"" + uri.getFragment() + "\"");
String attachmentFileName = null;
String[] projection = new String[]{OpenableColumns.DISPLAY_NAME};
try (Cursor c = getContentResolver().query(uri, projection, null, null, null)) {
if (c != null && c.moveToFirst()) {
final int fileNameColumnId = c.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (fileNameColumnId >= 0) attachmentFileName = c.getString(fileNameColumnId);
}
}
if (attachmentFileName == null) attachmentFileName = subjectFromIntent;
if (attachmentFileName == null) attachmentFileName = UriUtils.getUriFileBasename(uri, true);
InputStream in = getContentResolver().openInputStream(uri);
promptNameAndSave(in, attachmentFileName);
} catch (Exception e) {
showErrorDialogAndQuit("Unable to handle shared content:\n\n" + e.getMessage());
Logger.logStackTraceWithMessage(LOG_TAG, "handleContentUri(uri=" + uri + ") failed", e);
}
}
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;
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);
final Uri scriptUri = UriUtils.getFileUri(EDITOR_PROGRAM);
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, scriptUri);
executeIntent.setClass(FileReceiverActivity.this, TermuxService.class);
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()});
startService(executeIntent);
finish();
},
R.string.action_file_received_open_directory, text -> {
if (saveStreamWithName(in, text) == null) return;
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE);
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, TERMUX_RECEIVEDIR);
executeIntent.setClass(FileReceiverActivity.this, TermuxService.class);
startService(executeIntent);
finish();
},
android.R.string.cancel, text -> finish(), dialog -> {
if (mFinishOnDismissNameDialog) finish();
});
}
public File saveStreamWithName(InputStream in, String attachmentFileName) {
File receiveDir = new File(TERMUX_RECEIVEDIR);
if (DataUtils.isNullOrEmpty(attachmentFileName)) {
showErrorDialogAndQuit("File name cannot be null or empty");
return null;
}
if (!receiveDir.isDirectory() && !receiveDir.mkdirs()) {
showErrorDialogAndQuit("Cannot create directory: " + receiveDir.getAbsolutePath());
return null;
}
try {
final File outFile = new File(receiveDir, attachmentFileName);
try (FileOutputStream f = new FileOutputStream(outFile)) {
byte[] buffer = new byte[4096];
int readBytes;
while ((readBytes = in.read(buffer)) > 0) {
f.write(buffer, 0, readBytes);
}
}
return outFile;
} catch (IOException e) {
showErrorDialogAndQuit("Error saving file:\n\n" + e);
Logger.logStackTraceWithMessage(LOG_TAG, "Error saving file", e);
return null;
}
}
void handleUrlAndFinish(final String url) {
final File urlOpenerProgramFile = new File(URL_OPENER_PROGRAM);
if (!urlOpenerProgramFile.isFile()) {
showErrorDialogAndQuit("The following file does not exist:\n$HOME/bin/termux-url-opener\n\n"
+ "Create this file as a script or a symlink - it will be called with the shared URL as the first argument.");
return;
}
// Do this for the user if necessary:
//noinspection ResultOfMethodCallIgnored
urlOpenerProgramFile.setExecutable(true);
final Uri urlOpenerProgramUri = UriUtils.getFileUri(URL_OPENER_PROGRAM);
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, urlOpenerProgramUri);
executeIntent.setClass(FileReceiverActivity.this, TermuxService.class);
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, new String[]{url});
startService(executeIntent);
finish();
}
/**
* Update {@link TERMUX_APP#FILE_SHARE_RECEIVER_ACTIVITY_CLASS_NAME} component state depending on
* {@link TermuxPropertyConstants#KEY_DISABLE_FILE_SHARE_RECEIVER} value and
* {@link TERMUX_APP#FILE_VIEW_RECEIVER_ACTIVITY_CLASS_NAME} component state depending on
* {@link TermuxPropertyConstants#KEY_DISABLE_FILE_VIEW_RECEIVER} value.
*/
public static void updateFileReceiverActivityComponentsState(@NonNull Context context) {
new Thread() {
@Override
public void run() {
TermuxAppSharedProperties properties = TermuxAppSharedProperties.getProperties();
String errmsg;
boolean state;
state = !properties.isFileShareReceiverDisabled();
Logger.logVerbose(LOG_TAG, "Setting " + TERMUX_APP.FILE_SHARE_RECEIVER_ACTIVITY_CLASS_NAME + " component state to " + state);
errmsg = PackageUtils.setComponentState(context,TermuxConstants.TERMUX_PACKAGE_NAME,
TERMUX_APP.FILE_SHARE_RECEIVER_ACTIVITY_CLASS_NAME,
state, null, false, false);
if (errmsg != null)
Logger.logError(LOG_TAG, errmsg);
state = !properties.isFileViewReceiverDisabled();
Logger.logVerbose(LOG_TAG, "Setting " + TERMUX_APP.FILE_VIEW_RECEIVER_ACTIVITY_CLASS_NAME + " component state to " + state);
errmsg = PackageUtils.setComponentState(context,TermuxConstants.TERMUX_PACKAGE_NAME,
TERMUX_APP.FILE_VIEW_RECEIVER_ACTIVITY_CLASS_NAME,
state, null, false, false);
if (errmsg != null)
Logger.logError(LOG_TAG, errmsg);
}
}.start();
}
}

View File

@ -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());
}
}

View File

@ -1,10 +1,9 @@
package com.termux.app.fragments.settings.termux;
package com.termux.app.fragments.settings;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.ListPreference;
import androidx.preference.PreferenceCategory;
@ -13,7 +12,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
@ -21,32 +20,20 @@ public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(context));
preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(getContext()));
setPreferencesFromResource(R.xml.termux_debugging_preferences, rootKey);
setPreferencesFromResource(R.xml.debugging_preferences, rootKey);
configureLoggingPreferences(context);
}
private void configureLoggingPreferences(@NonNull Context context) {
PreferenceCategory loggingCategory = findPreference("logging");
if (loggingCategory == null) return;
ListPreference logLevelListPreference = findPreference("log_level");
if (logLevelListPreference != null) {
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context, true);
if (preferences == null) return;
setLogLevelListPreferenceData(logLevelListPreference, context, preferences.getLogLevel());
if (loggingCategory != null) {
final ListPreference logLevelListPreference = setLogLevelListPreferenceData(findPreference("log_level"), getActivity());
loggingCategory.addPreference(logLevelListPreference);
}
}
public static ListPreference setLogLevelListPreferenceData(ListPreference logLevelListPreference, Context context, int logLevel) {
protected ListPreference setLogLevelListPreferenceData(ListPreference logLevelListPreference, Context context) {
if (logLevelListPreference == null)
logLevelListPreference = new ListPreference(context);
@ -56,8 +43,8 @@ public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
logLevelListPreference.setEntryValues(logLevels);
logLevelListPreference.setEntries(logLevelLabels);
logLevelListPreference.setValue(String.valueOf(logLevel));
logLevelListPreference.setDefaultValue(Logger.DEFAULT_LOG_LEVEL);
logLevelListPreference.setValue(String.valueOf(Logger.getLogLevel()));
logLevelListPreference.setDefaultValue(Logger.getLogLevel());
return logLevelListPreference;
}
@ -73,12 +60,12 @@ class DebuggingPreferencesDataStore extends PreferenceDataStore {
private DebuggingPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxAppSharedPreferences.build(context, true);
mPreferences = new TermuxAppSharedPreferences(context);
}
public static synchronized DebuggingPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new DebuggingPreferencesDataStore(context);
mInstance = new DebuggingPreferencesDataStore(context.getApplicationContext());
}
return mInstance;
}
@ -88,7 +75,6 @@ class DebuggingPreferencesDataStore extends PreferenceDataStore {
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
if (mPreferences == null) return null;
if (key == null) return null;
switch (key) {
@ -101,7 +87,6 @@ class DebuggingPreferencesDataStore extends PreferenceDataStore {
@Override
public void putString(String key, @Nullable String value) {
if (mPreferences == null) return;
if (key == null) return;
switch (key) {
@ -119,7 +104,6 @@ class DebuggingPreferencesDataStore extends PreferenceDataStore {
@Override
public void putBoolean(String key, boolean value) {
if (mPreferences == null) return;
if (key == null) return;
switch (key) {
@ -139,14 +123,13 @@ class DebuggingPreferencesDataStore extends PreferenceDataStore {
@Override
public boolean getBoolean(String key, boolean defValue) {
if (mPreferences == null) return false;
switch (key) {
case "terminal_view_key_logging_enabled":
return mPreferences.isTerminalViewKeyLoggingEnabled();
return mPreferences.getTerminalViewKeyLoggingEnabled();
case "plugin_error_notifications_enabled":
return mPreferences.arePluginErrorNotificationsEnabled(false);
return mPreferences.getPluginErrorNotificationsEnabled();
case "crash_report_notifications_enabled":
return mPreferences.areCrashReportNotificationsEnabled(false);
return mPreferences.getCrashReportNotificationsEnabled();
default:
return false;
}

View File

@ -1,4 +1,4 @@
package com.termux.app.fragments.settings.termux;
package com.termux.app.fragments.settings;
import android.content.Context;
import android.os.Bundle;
@ -9,20 +9,17 @@ 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 {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(TerminalIOPreferencesDataStore.getInstance(context));
preferenceManager.setPreferenceDataStore(TerminalIOPreferencesDataStore.getInstance(getContext()));
setPreferencesFromResource(R.xml.termux_terminal_io_preferences, rootKey);
setPreferencesFromResource(R.xml.terminal_io_preferences, rootKey);
}
}
@ -36,12 +33,12 @@ class TerminalIOPreferencesDataStore extends PreferenceDataStore {
private TerminalIOPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxAppSharedPreferences.build(context, true);
mPreferences = new TermuxAppSharedPreferences(context);
}
public static synchronized TerminalIOPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new TerminalIOPreferencesDataStore(context);
mInstance = new TerminalIOPreferencesDataStore(context.getApplicationContext());
}
return mInstance;
}
@ -50,16 +47,12 @@ class TerminalIOPreferencesDataStore extends PreferenceDataStore {
@Override
public void putBoolean(String key, boolean value) {
if (mPreferences == null) return;
if (key == null) return;
switch (key) {
case "soft_keyboard_enabled":
mPreferences.setSoftKeyboardEnabled(value);
break;
case "soft_keyboard_enabled_only_if_no_hardware":
mPreferences.setSoftKeyboardEnabledOnlyIfNoHardware(value);
break;
default:
break;
}
@ -67,13 +60,9 @@ class TerminalIOPreferencesDataStore extends PreferenceDataStore {
@Override
public boolean getBoolean(String key, boolean defValue) {
if (mPreferences == null) return false;
switch (key) {
case "soft_keyboard_enabled":
return mPreferences.isSoftKeyboardEnabled();
case "soft_keyboard_enabled_only_if_no_hardware":
return mPreferences.isSoftKeyboardEnabledOnlyIfNoHardware();
return mPreferences.getSoftKeyboardEnabled();
default:
return false;
}

View File

@ -1,49 +0,0 @@
package com.termux.app.fragments.settings;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Keep;
import androidx.preference.PreferenceDataStore;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import com.termux.R;
import com.termux.shared.termux.settings.preferences.TermuxAPIAppSharedPreferences;
@Keep
public class TermuxAPIPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(TermuxAPIPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.termux_api_preferences, rootKey);
}
}
class TermuxAPIPreferencesDataStore extends PreferenceDataStore {
private final Context mContext;
private final TermuxAPIAppSharedPreferences mPreferences;
private static TermuxAPIPreferencesDataStore mInstance;
private TermuxAPIPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxAPIAppSharedPreferences.build(context, true);
}
public static synchronized TermuxAPIPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new TermuxAPIPreferencesDataStore(context);
}
return mInstance;
}
}

View File

@ -1,49 +0,0 @@
package com.termux.app.fragments.settings;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Keep;
import androidx.preference.PreferenceDataStore;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import com.termux.R;
import com.termux.shared.termux.settings.preferences.TermuxFloatAppSharedPreferences;
@Keep
public class TermuxFloatPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(TermuxFloatPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.termux_float_preferences, rootKey);
}
}
class TermuxFloatPreferencesDataStore extends PreferenceDataStore {
private final Context mContext;
private final TermuxFloatAppSharedPreferences mPreferences;
private static TermuxFloatPreferencesDataStore mInstance;
private TermuxFloatPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxFloatAppSharedPreferences.build(context, true);
}
public static synchronized TermuxFloatPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new TermuxFloatPreferencesDataStore(context);
}
return mInstance;
}
}

View File

@ -1,49 +0,0 @@
package com.termux.app.fragments.settings;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Keep;
import androidx.preference.PreferenceDataStore;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import com.termux.R;
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
@Keep
public class TermuxPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(TermuxPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.termux_preferences, rootKey);
}
}
class TermuxPreferencesDataStore extends PreferenceDataStore {
private final Context mContext;
private final TermuxAppSharedPreferences mPreferences;
private static TermuxPreferencesDataStore mInstance;
private TermuxPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxAppSharedPreferences.build(context, true);
}
public static synchronized TermuxPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new TermuxPreferencesDataStore(context);
}
return mInstance;
}
}

View File

@ -1,49 +0,0 @@
package com.termux.app.fragments.settings;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Keep;
import androidx.preference.PreferenceDataStore;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import com.termux.R;
import com.termux.shared.termux.settings.preferences.TermuxTaskerAppSharedPreferences;
@Keep
public class TermuxTaskerPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(TermuxTaskerPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.termux_tasker_preferences, rootKey);
}
}
class TermuxTaskerPreferencesDataStore extends PreferenceDataStore {
private final Context mContext;
private final TermuxTaskerAppSharedPreferences mPreferences;
private static TermuxTaskerPreferencesDataStore mInstance;
private TermuxTaskerPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxTaskerAppSharedPreferences.build(context, true);
}
public static synchronized TermuxTaskerPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new TermuxTaskerPreferencesDataStore(context);
}
return mInstance;
}
}

View File

@ -1,49 +0,0 @@
package com.termux.app.fragments.settings;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Keep;
import androidx.preference.PreferenceDataStore;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import com.termux.R;
import com.termux.shared.termux.settings.preferences.TermuxWidgetAppSharedPreferences;
@Keep
public class TermuxWidgetPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(TermuxWidgetPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.termux_widget_preferences, rootKey);
}
}
class TermuxWidgetPreferencesDataStore extends PreferenceDataStore {
private final Context mContext;
private final TermuxWidgetAppSharedPreferences mPreferences;
private static TermuxWidgetPreferencesDataStore mInstance;
private TermuxWidgetPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxWidgetAppSharedPreferences.build(context, true);
}
public static synchronized TermuxWidgetPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new TermuxWidgetPreferencesDataStore(context);
}
return mInstance;
}
}

View File

@ -1,77 +0,0 @@
package com.termux.app.fragments.settings.termux;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Keep;
import androidx.preference.PreferenceDataStore;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import com.termux.R;
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
@Keep
public class TerminalViewPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(TerminalViewPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.termux_terminal_view_preferences, rootKey);
}
}
class TerminalViewPreferencesDataStore extends PreferenceDataStore {
private final Context mContext;
private final TermuxAppSharedPreferences mPreferences;
private static TerminalViewPreferencesDataStore mInstance;
private TerminalViewPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxAppSharedPreferences.build(context, true);
}
public static synchronized TerminalViewPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new TerminalViewPreferencesDataStore(context);
}
return mInstance;
}
@Override
public void putBoolean(String key, boolean value) {
if (mPreferences == null) return;
if (key == null) return;
switch (key) {
case "terminal_margin_adjustment":
mPreferences.setTerminalMarginAdjustment(value);
break;
default:
break;
}
}
@Override
public boolean getBoolean(String key, boolean defValue) {
if (mPreferences == null) return false;
switch (key) {
case "terminal_margin_adjustment":
return mPreferences.isTerminalMarginAdjustmentEnabled();
default:
return false;
}
}
}

View File

@ -1,101 +0,0 @@
package com.termux.app.fragments.settings.termux_api;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.ListPreference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceDataStore;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import com.termux.R;
import com.termux.shared.termux.settings.preferences.TermuxAPIAppSharedPreferences;
@Keep
public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.termux_api_debugging_preferences, rootKey);
configureLoggingPreferences(context);
}
private void configureLoggingPreferences(@NonNull Context context) {
PreferenceCategory loggingCategory = findPreference("logging");
if (loggingCategory == null) return;
ListPreference logLevelListPreference = findPreference("log_level");
if (logLevelListPreference != null) {
TermuxAPIAppSharedPreferences preferences = TermuxAPIAppSharedPreferences.build(context, true);
if (preferences == null) return;
com.termux.app.fragments.settings.termux.DebuggingPreferencesFragment.
setLogLevelListPreferenceData(logLevelListPreference, context, preferences.getLogLevel(true));
loggingCategory.addPreference(logLevelListPreference);
}
}
}
class DebuggingPreferencesDataStore extends PreferenceDataStore {
private final Context mContext;
private final TermuxAPIAppSharedPreferences mPreferences;
private static DebuggingPreferencesDataStore mInstance;
private DebuggingPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxAPIAppSharedPreferences.build(context, true);
}
public static synchronized DebuggingPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new DebuggingPreferencesDataStore(context);
}
return mInstance;
}
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
if (mPreferences == null) return null;
if (key == null) return null;
switch (key) {
case "log_level":
return String.valueOf(mPreferences.getLogLevel(true));
default:
return null;
}
}
@Override
public void putString(String key, @Nullable String value) {
if (mPreferences == null) return;
if (key == null) return;
switch (key) {
case "log_level":
if (value != null) {
mPreferences.setLogLevel(mContext, Integer.parseInt(value), true);
}
break;
default:
break;
}
}
}

View File

@ -1,126 +0,0 @@
package com.termux.app.fragments.settings.termux_float;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.ListPreference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceDataStore;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import com.termux.R;
import com.termux.shared.termux.settings.preferences.TermuxFloatAppSharedPreferences;
@Keep
public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.termux_float_debugging_preferences, rootKey);
configureLoggingPreferences(context);
}
private void configureLoggingPreferences(@NonNull Context context) {
PreferenceCategory loggingCategory = findPreference("logging");
if (loggingCategory == null) return;
ListPreference logLevelListPreference = findPreference("log_level");
if (logLevelListPreference != null) {
TermuxFloatAppSharedPreferences preferences = TermuxFloatAppSharedPreferences.build(context, true);
if (preferences == null) return;
com.termux.app.fragments.settings.termux.DebuggingPreferencesFragment.
setLogLevelListPreferenceData(logLevelListPreference, context, preferences.getLogLevel(true));
loggingCategory.addPreference(logLevelListPreference);
}
}
}
class DebuggingPreferencesDataStore extends PreferenceDataStore {
private final Context mContext;
private final TermuxFloatAppSharedPreferences mPreferences;
private static DebuggingPreferencesDataStore mInstance;
private DebuggingPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxFloatAppSharedPreferences.build(context, true);
}
public static synchronized DebuggingPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new DebuggingPreferencesDataStore(context);
}
return mInstance;
}
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
if (mPreferences == null) return null;
if (key == null) return null;
switch (key) {
case "log_level":
return String.valueOf(mPreferences.getLogLevel(true));
default:
return null;
}
}
@Override
public void putString(String key, @Nullable String value) {
if (mPreferences == null) return;
if (key == null) return;
switch (key) {
case "log_level":
if (value != null) {
mPreferences.setLogLevel(mContext, Integer.parseInt(value), true);
}
break;
default:
break;
}
}
@Override
public void putBoolean(String key, boolean value) {
if (mPreferences == null) return;
if (key == null) return;
switch (key) {
case "terminal_view_key_logging_enabled":
mPreferences.setTerminalViewKeyLoggingEnabled(value, true);
break;
default:
break;
}
}
@Override
public boolean getBoolean(String key, boolean defValue) {
if (mPreferences == null) return false;
switch (key) {
case "terminal_view_key_logging_enabled":
return mPreferences.isTerminalViewKeyLoggingEnabled(true);
default:
return false;
}
}
}

View File

@ -1,101 +0,0 @@
package com.termux.app.fragments.settings.termux_tasker;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.ListPreference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceDataStore;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import com.termux.R;
import com.termux.shared.termux.settings.preferences.TermuxTaskerAppSharedPreferences;
@Keep
public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.termux_tasker_debugging_preferences, rootKey);
configureLoggingPreferences(context);
}
private void configureLoggingPreferences(@NonNull Context context) {
PreferenceCategory loggingCategory = findPreference("logging");
if (loggingCategory == null) return;
ListPreference logLevelListPreference = findPreference("log_level");
if (logLevelListPreference != null) {
TermuxTaskerAppSharedPreferences preferences = TermuxTaskerAppSharedPreferences.build(context, true);
if (preferences == null) return;
com.termux.app.fragments.settings.termux.DebuggingPreferencesFragment.
setLogLevelListPreferenceData(logLevelListPreference, context, preferences.getLogLevel(true));
loggingCategory.addPreference(logLevelListPreference);
}
}
}
class DebuggingPreferencesDataStore extends PreferenceDataStore {
private final Context mContext;
private final TermuxTaskerAppSharedPreferences mPreferences;
private static DebuggingPreferencesDataStore mInstance;
private DebuggingPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxTaskerAppSharedPreferences.build(context, true);
}
public static synchronized DebuggingPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new DebuggingPreferencesDataStore(context);
}
return mInstance;
}
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
if (mPreferences == null) return null;
if (key == null) return null;
switch (key) {
case "log_level":
return String.valueOf(mPreferences.getLogLevel(true));
default:
return null;
}
}
@Override
public void putString(String key, @Nullable String value) {
if (mPreferences == null) return;
if (key == null) return;
switch (key) {
case "log_level":
if (value != null) {
mPreferences.setLogLevel(mContext, Integer.parseInt(value), true);
}
break;
default:
break;
}
}
}

View File

@ -1,101 +0,0 @@
package com.termux.app.fragments.settings.termux_widget;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.ListPreference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceDataStore;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import com.termux.R;
import com.termux.shared.termux.settings.preferences.TermuxWidgetAppSharedPreferences;
@Keep
public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.termux_widget_debugging_preferences, rootKey);
configureLoggingPreferences(context);
}
private void configureLoggingPreferences(@NonNull Context context) {
PreferenceCategory loggingCategory = findPreference("logging");
if (loggingCategory == null) return;
ListPreference logLevelListPreference = findPreference("log_level");
if (logLevelListPreference != null) {
TermuxWidgetAppSharedPreferences preferences = TermuxWidgetAppSharedPreferences.build(context, true);
if (preferences == null) return;
com.termux.app.fragments.settings.termux.DebuggingPreferencesFragment.
setLogLevelListPreferenceData(logLevelListPreference, context, preferences.getLogLevel(true));
loggingCategory.addPreference(logLevelListPreference);
}
}
}
class DebuggingPreferencesDataStore extends PreferenceDataStore {
private final Context mContext;
private final TermuxWidgetAppSharedPreferences mPreferences;
private static DebuggingPreferencesDataStore mInstance;
private DebuggingPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxWidgetAppSharedPreferences.build(context, true);
}
public static synchronized DebuggingPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new DebuggingPreferencesDataStore(context);
}
return mInstance;
}
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
if (mPreferences == null) return null;
if (key == null) return null;
switch (key) {
case "log_level":
return String.valueOf(mPreferences.getLogLevel(true));
default:
return null;
}
}
@Override
public void putString(String key, @Nullable String value) {
if (mPreferences == null) return;
if (key == null) return;
switch (key) {
case "log_level":
if (value != null) {
mPreferences.setLogLevel(mContext, Integer.parseInt(value), true);
}
break;
default:
break;
}
}
}

View File

@ -0,0 +1,64 @@
package com.termux.app.models;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.termux.TermuxUtils;
import java.io.Serializable;
public class ReportInfo implements Serializable {
/** The user action that was being processed for which the report was generated. */
public final UserAction userAction;
/** The internal app component that sent the report. */
public final String sender;
/** The report title. */
public final String reportTitle;
/** The markdown report text prefix. Will not be part of copy and share operations, etc. */
public final String reportStringPrefix;
/** The markdown report text. */
public final String reportString;
/** The markdown report text suffix. Will not be part of copy and share operations, etc. */
public final String reportStringSuffix;
/** If set to {@code true}, then report, app and device info will be added to the report when
* markdown is generated.
*/
public final boolean addReportInfoToMarkdown;
/** The timestamp for the report. */
public final String reportTimestamp;
public ReportInfo(UserAction userAction, String sender, String reportTitle, String reportStringPrefix, String reportString, String reportStringSuffix, boolean addReportInfoToMarkdown) {
this.userAction = userAction;
this.sender = sender;
this.reportTitle = reportTitle;
this.reportStringPrefix = reportStringPrefix;
this.reportString = reportString;
this.reportStringSuffix = reportStringSuffix;
this.addReportInfoToMarkdown = addReportInfoToMarkdown;
this.reportTimestamp = TermuxUtils.getCurrentTimeStamp();
}
/**
* Get a markdown {@link String} for {@link ReportInfo}.
*
* @param reportInfo The {@link ReportInfo} to convert.
* @return Returns the markdown {@link String}.
*/
public static String getReportInfoMarkdownString(final ReportInfo reportInfo) {
if (reportInfo == null) return "null";
StringBuilder markdownString = new StringBuilder();
if (reportInfo.addReportInfoToMarkdown) {
markdownString.append("## Report Info\n\n");
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("User Action", reportInfo.userAction, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Sender", reportInfo.sender, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Report Timestamp", reportInfo.reportTimestamp, "-"));
markdownString.append("\n##\n\n");
}
markdownString.append(reportInfo.reportString);
return markdownString.toString();
}
}

View File

@ -2,7 +2,8 @@ package com.termux.app.models;
public enum UserAction {
ABOUT("about"),
PLUGIN_EXECUTION_COMMAND("plugin execution command"),
CRASH_REPORT("crash report"),
REPORT_ISSUE_FROM_TRANSCRIPT("report issue from transcript");
private final String name;

View File

@ -0,0 +1,99 @@
package com.termux.app.settings.properties;
import android.content.Context;
import com.termux.app.terminal.io.KeyboardShortcut;
import com.termux.app.terminal.io.extrakeys.ExtraKeysInfo;
import com.termux.shared.logger.Logger;
import com.termux.shared.settings.properties.SharedPropertiesParser;
import com.termux.shared.settings.properties.TermuxPropertyConstants;
import com.termux.shared.settings.properties.TermuxSharedProperties;
import org.json.JSONException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;
public class TermuxAppSharedProperties extends TermuxSharedProperties implements SharedPropertiesParser {
private ExtraKeysInfo mExtraKeysInfo;
private List<KeyboardShortcut> mSessionShortcuts = new ArrayList<>();
private static final String LOG_TAG = "TermuxAppSharedProperties";
public TermuxAppSharedProperties(@Nonnull Context context) {
super(context);
}
/**
* Reload the termux properties from disk into an in-memory cache.
*/
@Override
public void loadTermuxPropertiesFromDisk() {
super.loadTermuxPropertiesFromDisk();
setExtraKeys();
setSessionShortcuts();
}
/**
* Set the terminal extra keys and style.
*/
private void setExtraKeys() {
mExtraKeysInfo = null;
try {
// The mMap stores the extra key and style string values while loading properties
// Check {@link #getExtraKeysInternalPropertyValueFromValue(String)} and
// {@link #getExtraKeysStyleInternalPropertyValueFromValue(String)}
String extrakeys = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS, true);
String extraKeysStyle = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE, true);
mExtraKeysInfo = new ExtraKeysInfo(extrakeys, extraKeysStyle);
} catch (JSONException e) {
Logger.showToast(mContext, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: " + e.toString(), true);
Logger.logStackTraceWithMessage(LOG_TAG, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: ", e);
try {
mExtraKeysInfo = new ExtraKeysInfo(TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE);
} catch (JSONException e2) {
Logger.showToast(mContext, "Can't create default extra keys",true);
Logger.logStackTraceWithMessage(LOG_TAG, "Could create default extra keys: ", e);
mExtraKeysInfo = null;
}
}
}
/**
* Set the terminal sessions shortcuts.
*/
private void setSessionShortcuts() {
if (mSessionShortcuts == null)
mSessionShortcuts = new ArrayList<>();
else
mSessionShortcuts.clear();
// The {@link TermuxPropertyConstants#MAP_SESSION_SHORTCUTS} stores the session shortcut key and action pair
for (Map.Entry<String, Integer> entry : TermuxPropertyConstants.MAP_SESSION_SHORTCUTS.entrySet()) {
// The mMap stores the code points for the session shortcuts while loading properties
Integer codePoint = (Integer) getInternalPropertyValue(entry.getKey(), true);
// If codePoint is null, then session shortcut did not exist in properties or was invalid
// as parsed by {@link #getCodePointForSessionShortcuts(String,String)}
// If codePoint is not null, then get the action for the MAP_SESSION_SHORTCUTS key and
// add the code point to sessionShortcuts
if (codePoint != null)
mSessionShortcuts.add(new KeyboardShortcut(codePoint, entry.getValue()));
}
}
public List<KeyboardShortcut> getSessionShortcuts() {
return mSessionShortcuts;
}
public ExtraKeysInfo getExtraKeysInfo() {
return mExtraKeysInfo;
}
}

View File

@ -1,284 +0,0 @@
package com.termux.app.terminal;
import android.content.Context;
import android.graphics.Rect;
import android.inputmethodservice.InputMethodService;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.view.inputmethod.EditorInfo;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import androidx.core.view.WindowInsetsCompat;
import com.termux.app.TermuxActivity;
import com.termux.shared.logger.Logger;
import com.termux.shared.view.ViewUtils;
/**
* The {@link TermuxActivity} relies on {@link android.view.WindowManager.LayoutParams#SOFT_INPUT_ADJUST_RESIZE)}
* set by {@link TermuxTerminalViewClient#setSoftKeyboardState(boolean, boolean)} to automatically
* resize the view and push the terminal up when soft keyboard is opened. However, this does not
* always work properly. When `enforce-char-based-input=true` is set in `termux.properties`
* and {@link com.termux.view.TerminalView#onCreateInputConnection(EditorInfo)} sets the inputType
* to `InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS`
* instead of the default `InputType.TYPE_NULL` for termux, some keyboards may still show suggestions.
* Gboard does too, but only when text is copied and clipboard suggestions **and** number keys row
* toggles are enabled in its settings. When number keys row toggle is not enabled, Gboard will still
* show the row but will switch it with suggestions if needed. If its enabled, then number keys row
* is always shown and suggestions are shown in an additional row on top of it. This additional row is likely
* part of the candidates view returned by the keyboard app in {@link InputMethodService#onCreateCandidatesView()}.
*
* With the above configuration, the additional clipboard suggestions row partially covers the
* extra keys/terminal. Reopening the keyboard/activity does not fix the issue. This is either a bug
* in the Android OS where it does not consider the candidate's view height in its calculation to push
* up the view or because Gboard does not include the candidate's view height in the height reported
* to android that should be used, hence causing an overlap.
*
* Gboard logs the following entry to `logcat` when its opened with or without the suggestions bar showing:
* I/KeyboardViewUtil: KeyboardViewUtil.calculateMaxKeyboardBodyHeight():62 leave 500 height for app when screen height:2392, header height:176 and isFullscreenMode:false, so the max keyboard body height is:1716
* where `keyboard_height = screen_height - height_for_app - header_height` (62 is a hardcoded value in Gboard source code and may be a version number)
* So this may in fact be due to Gboard but https://stackoverflow.com/questions/57567272 suggests
* otherwise. Another similar report https://stackoverflow.com/questions/66761661.
* Also check https://github.com/termux/termux-app/issues/1539.
*
* This overlap may happen even without `enforce-char-based-input=true` for keyboards with extended layouts
* like number row, etc.
*
* To fix these issues, `activity_termux.xml` has the constant 1sp transparent
* `activity_termux_bottom_space_view` View at the bottom. This will appear as a line matching the
* activity theme. When {@link TermuxActivity} {@link ViewTreeObserver.OnGlobalLayoutListener} is
* called when any of the sub view layouts change, like keyboard opening/closing keyboard,
* extra keys/input view switched, etc, we check if the bottom space view is visible or not.
* If its not, then we add a margin to the bottom of the root view, so that the keyboard does not
* overlap the extra keys/terminal, since the margin will push up the view. By default the margin
* added is equal to the height of the hidden part of extra keys/terminal. For Gboard's case, the
* hidden part equals the `header_height`. The updates to margins may cause a jitter in some cases
* when the view is redrawn if the margin is incorrect, but logic has been implemented to avoid that.
*/
public class TermuxActivityRootView extends LinearLayout implements ViewTreeObserver.OnGlobalLayoutListener {
public TermuxActivity mActivity;
public Integer marginBottom;
public Integer lastMarginBottom;
public long lastMarginBottomTime;
public long lastMarginBottomExtraTime;
/** Log root view events. */
private boolean ROOT_VIEW_LOGGING_ENABLED = false;
private static final String LOG_TAG = "TermuxActivityRootView";
private static int mStatusBarHeight;
public TermuxActivityRootView(Context context) {
super(context);
}
public TermuxActivityRootView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public TermuxActivityRootView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setActivity(TermuxActivity activity) {
mActivity = activity;
}
/**
* Sets whether root view logging is enabled or not.
*
* @param value The boolean value that defines the state.
*/
public void setIsRootViewLoggingEnabled(boolean value) {
ROOT_VIEW_LOGGING_ENABLED = value;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (marginBottom != null) {
if (ROOT_VIEW_LOGGING_ENABLED)
Logger.logVerbose(LOG_TAG, "onMeasure: Setting bottom margin to " + marginBottom);
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) getLayoutParams();
params.setMargins(0, 0, 0, marginBottom);
setLayoutParams(params);
marginBottom = null;
requestLayout();
}
}
@Override
public void onGlobalLayout() {
if (mActivity == null || !mActivity.isVisible()) return;
View bottomSpaceView = mActivity.getTermuxActivityBottomSpaceView();
if (bottomSpaceView == null) return;
boolean root_view_logging_enabled = ROOT_VIEW_LOGGING_ENABLED;
if (root_view_logging_enabled)
Logger.logVerbose(LOG_TAG, ":\nonGlobalLayout:");
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) getLayoutParams();
// Get the position Rects of the bottom space view and the main window holding it
Rect[] windowAndViewRects = ViewUtils.getWindowAndViewRects(bottomSpaceView, mStatusBarHeight);
if (windowAndViewRects == null)
return;
Rect windowAvailableRect = windowAndViewRects[0];
Rect bottomSpaceViewRect = windowAndViewRects[1];
// If the bottomSpaceViewRect is inside the windowAvailableRect, then it must be completely visible
//boolean isVisible = windowAvailableRect.contains(bottomSpaceViewRect); // rect.right comparison often fails in landscape
boolean isVisible = ViewUtils.isRectAbove(windowAvailableRect, bottomSpaceViewRect);
boolean isVisibleBecauseMargin = (windowAvailableRect.bottom == bottomSpaceViewRect.bottom) && params.bottomMargin > 0;
boolean isVisibleBecauseExtraMargin = ((bottomSpaceViewRect.bottom - windowAvailableRect.bottom) < 0);
if (root_view_logging_enabled) {
Logger.logVerbose(LOG_TAG, "windowAvailableRect " + ViewUtils.toRectString(windowAvailableRect) + ", bottomSpaceViewRect " + ViewUtils.toRectString(bottomSpaceViewRect));
Logger.logVerbose(LOG_TAG, "windowAvailableRect.bottom " + windowAvailableRect.bottom +
", bottomSpaceViewRect.bottom " +bottomSpaceViewRect.bottom +
", diff " + (bottomSpaceViewRect.bottom - windowAvailableRect.bottom) + ", bottom " + params.bottomMargin +
", isVisible " + windowAvailableRect.contains(bottomSpaceViewRect) + ", isRectAbove " + ViewUtils.isRectAbove(windowAvailableRect, bottomSpaceViewRect) +
", isVisibleBecauseMargin " + isVisibleBecauseMargin + ", isVisibleBecauseExtraMargin " + isVisibleBecauseExtraMargin);
}
// If the bottomSpaceViewRect is visible, then remove the margin if needed
if (isVisible) {
// If visible because of margin, i.e the bottom of bottomSpaceViewRect equals that of windowAvailableRect
// and a margin has been added
// Necessary so that we don't get stuck in an infinite loop since setting margin
// will call OnGlobalLayoutListener again and next time bottom space view
// will be visible and margin will be set to 0, which again will call
// OnGlobalLayoutListener...
// Calling addTermuxActivityRootViewGlobalLayoutListener with a delay fails to
// set appropriate margins when views are changed quickly since some changes
// may be missed.
if (isVisibleBecauseMargin) {
if (root_view_logging_enabled)
Logger.logVerbose(LOG_TAG, "Visible due to margin");
// Once the view has been redrawn with new margin, we set margin back to 0 so that
// when next time onMeasure() is called, margin 0 is used. This is necessary for
// cases when view has been redrawn with new margin because bottom space view was
// hidden by keyboard and then view was redrawn again due to layout change (like
// keyboard symbol view is switched to), android will add margin below its new position
// if its greater than 0, which was already above the keyboard creating x2x margin.
// Adding time check since moving split screen divider in landscape causes jitter
// and prevents some infinite loops
if ((System.currentTimeMillis() - lastMarginBottomTime) > 40) {
lastMarginBottomTime = System.currentTimeMillis();
marginBottom = 0;
} else {
if (root_view_logging_enabled)
Logger.logVerbose(LOG_TAG, "Ignoring restoring marginBottom to 0 since called to quickly");
}
return;
}
boolean setMargin = params.bottomMargin != 0;
// If visible because of extra margin, i.e the bottom of bottomSpaceViewRect is above that of windowAvailableRect
// onGlobalLayout: windowAvailableRect 1408, bottomSpaceViewRect 1232, diff -176, bottom 0, isVisible true, isVisibleBecauseMargin false, isVisibleBecauseExtraMargin false
// onGlobalLayout: Bottom margin already equals 0
if (isVisibleBecauseExtraMargin) {
// Adding time check since prevents infinite loops, like in landscape mode in freeform mode in Taskbar
if ((System.currentTimeMillis() - lastMarginBottomExtraTime) > 40) {
if (root_view_logging_enabled)
Logger.logVerbose(LOG_TAG, "Resetting margin since visible due to extra margin");
lastMarginBottomExtraTime = System.currentTimeMillis();
// lastMarginBottom must be invalid. May also happen when keyboards are changed.
lastMarginBottom = null;
setMargin = true;
} else {
if (root_view_logging_enabled)
Logger.logVerbose(LOG_TAG, "Ignoring resetting margin since visible due to extra margin since called to quickly");
}
}
if (setMargin) {
if (root_view_logging_enabled)
Logger.logVerbose(LOG_TAG, "Setting bottom margin to 0");
params.setMargins(0, 0, 0, 0);
setLayoutParams(params);
} else {
if (root_view_logging_enabled)
Logger.logVerbose(LOG_TAG, "Bottom margin already equals 0");
// This is done so that when next time onMeasure() is called, lastMarginBottom is used.
// This is done since we **expect** the keyboard to have same dimensions next time layout
// changes, so best set margin while view is drawn the first time, otherwise it will
// cause a jitter when OnGlobalLayoutListener is called with margin 0 and it sets the
// likely same lastMarginBottom again and requesting a redraw. Hopefully, this logic
// works fine for all cases.
marginBottom = lastMarginBottom;
}
}
// ELse find the part of the extra keys/terminal that is hidden and add a margin accordingly
else {
int pxHidden = bottomSpaceViewRect.bottom - windowAvailableRect.bottom;
if (root_view_logging_enabled)
Logger.logVerbose(LOG_TAG, "pxHidden " + pxHidden + ", bottom " + params.bottomMargin);
boolean setMargin = params.bottomMargin != pxHidden;
// If invisible despite margin, i.e a margin was added, but the bottom of bottomSpaceViewRect
// is still below that of windowAvailableRect, this will trigger OnGlobalLayoutListener
// again, so that margins are set properly. May happen when toolbar/extra keys is disabled
// and enabled from left drawer, just like case for isVisibleBecauseExtraMargin.
// onMeasure: Setting bottom margin to 176
// onGlobalLayout: windowAvailableRect 1232, bottomSpaceViewRect 1408, diff 176, bottom 176, isVisible false, isVisibleBecauseMargin false, isVisibleBecauseExtraMargin false
// onGlobalLayout: Bottom margin already equals 176
if (pxHidden > 0 && params.bottomMargin > 0) {
if (pxHidden != params.bottomMargin) {
if (root_view_logging_enabled)
Logger.logVerbose(LOG_TAG, "Force setting margin to 0 since not visible due to wrong margin");
pxHidden = 0;
} else {
if (root_view_logging_enabled)
Logger.logVerbose(LOG_TAG, "Force setting margin since not visible despite required margin");
}
setMargin = true;
}
if (pxHidden < 0) {
if (root_view_logging_enabled)
Logger.logVerbose(LOG_TAG, "Force setting margin to 0 since new margin is negative");
pxHidden = 0;
}
if (setMargin) {
if (root_view_logging_enabled)
Logger.logVerbose(LOG_TAG, "Setting bottom margin to " + pxHidden);
params.setMargins(0, 0, 0, pxHidden);
setLayoutParams(params);
lastMarginBottom = pxHidden;
} else {
if (root_view_logging_enabled)
Logger.logVerbose(LOG_TAG, "Bottom margin already equals " + pxHidden);
}
}
}
public static class WindowInsetsListener implements View.OnApplyWindowInsetsListener {
@Override
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
mStatusBarHeight = WindowInsetsCompat.toWindowInsetsCompat(insets).getInsets(WindowInsetsCompat.Type.statusBars()).top;
// Let view window handle insets however it wants
return v.onApplyWindowInsets(insets);
}
}
}

View File

@ -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;

View File

@ -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,18 @@ 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.interact.ShareUtils;
import com.termux.shared.termux.shell.command.runner.terminal.TermuxSession;
import com.termux.shared.termux.interact.TextInputDialogUtils;
import com.termux.shared.shell.TermuxSession;
import com.termux.shared.interact.DialogUtils;
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.app.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,92 +31,35 @@ 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;
private static final int MAX_SESSIONS = 8;
private SoundPool mBellSoundPool;
private final SoundPool mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
private int mBellSoundId;
private final 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;
mBellSoundId = mBellSoundPool.load(activity, R.raw.bell, 1);
}
/**
* Should be called when mActivity.onCreate() is called
*/
public void onCreate() {
// Set terminal fonts and colors
checkForFontAndColors();
}
/**
* Should be called when mActivity.onStart() is called
*/
public void onStart() {
// The service has connected, but data may have changed since we were last in the foreground.
// Get the session stored in shared preferences stored by {@link #onStop} if its valid,
// otherwise get the last session currently running.
if (mActivity.getTermuxService() != null) {
setCurrentSession(getCurrentStoredSessionOrLast());
termuxSessionListNotifyUpdated();
}
// The current terminal session may have changed while being away, force
// a refresh of the displayed terminal.
mActivity.getTerminalView().onScreenUpdated();
}
/**
* Should be called when mActivity.onResume() is called
*/
public void onResume() {
// 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();
}
/**
* Should be called when mActivity.onStop() is called
*/
public void onStop() {
// Store current session in shared preferences so that it can be restored later in
// {@link #onStart} if needed.
setCurrentStoredSession();
// Release mBellSoundPool resources, specially to prevent exceptions like the following to be thrown
// java.util.concurrent.TimeoutException: android.media.SoundPool.finalize() timed out after 10 seconds
// Bell is not played in background anyways
// Related: https://stackoverflow.com/a/28708351/14686958
releaseBellSoundPool();
}
/**
* Should be called when mActivity.reloadActivityStyling() is called
*/
public void onReloadActivityStyling() {
// Set terminal fonts and colors
checkForFontAndColors();
}
@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 +73,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()) {
@ -145,59 +82,39 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
return;
}
int index = service.getIndexOfSession(finishedSession);
// For plugin commands that expect the result back, we should immediately close the session
// and send the result back instead of waiting fo the user to press enter.
// The plugin can handle/show errors itself.
boolean isPluginExecutionCommandWithPendingResult = false;
TermuxSession termuxSession = service.getTermuxSession(index);
if (termuxSession != null) {
isPluginExecutionCommandWithPendingResult = termuxSession.getExecutionCommand().isPluginExecutionCommandWithPendingResult();
if (isPluginExecutionCommandWithPendingResult)
Logger.logVerbose(LOG_TAG, "The \"" + finishedSession.mSessionName + "\" session will be force finished automatically since result in pending.");
}
if (mActivity.isVisible() && finishedSession != mActivity.getCurrentSession()) {
// Show toast for non-current sessions that exit.
int indexOfSession = service.getIndexOfSession(finishedSession);
// Verify that session was not removed before we got told about it finishing:
if (index >= 0)
if (indexOfSession >= 0)
mActivity.showToast(toToastTitle(finishedSession) + " - exited", true);
}
if (mActivity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
// On Android TV devices we need to use older behaviour because we may
// not be able to have multiple launcher icons.
if (service.getTermuxSessionsSize() > 1 || isPluginExecutionCommandWithPendingResult) {
if (service.getTermuxSessionsSize() > 1) {
removeFinishedSession(finishedSession);
}
} else {
// Once we have a separate launcher icon for the failsafe session, it
// should be safe to auto-close session on exit code '0' or '130'.
if (finishedSession.getExitStatus() == 0 || finishedSession.getExitStatus() == 130 || isPluginExecutionCommandWithPendingResult) {
if (finishedSession.getExitStatus() == 0 || finishedSession.getExitStatus() == 130) {
removeFinishedSession(finishedSession);
}
}
}
@Override
public void onCopyTextToClipboard(@NonNull TerminalSession session, String text) {
public void onClipboardText(TerminalSession session, String text) {
if (!mActivity.isVisible()) return;
ShareUtils.copyTextToClipboard(mActivity, text);
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(text)));
}
@Override
public void onPasteTextFromClipboard(@Nullable TerminalSession session) {
if (!mActivity.isVisible()) return;
String text = ShareUtils.getTextStringFromClipboardIfSet(mActivity, true);
if (text != null)
mActivity.getTerminalView().mEmulator.paste(text);
}
@Override
public void onBell(@NonNull TerminalSession session) {
public void onBell(TerminalSession session) {
if (!mActivity.isVisible()) return;
switch (mActivity.getProperties().getBellBehaviour()) {
@ -205,88 +122,21 @@ 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);
mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
break;
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_IGNORE:
// Ignore the bell character.
break;
}
}
@Override
public void onColorsChanged(@NonNull TerminalSession changedSession) {
public void onColorsChanged(TerminalSession changedSession) {
if (mActivity.getCurrentSession() == changedSession)
updateBackgroundColor();
}
@Override
public void onTerminalCursorStateChange(boolean enabled) {
// Do not start cursor blinking thread if activity is not visible
if (enabled && !mActivity.isVisible()) {
Logger.logVerbose(LOG_TAG, "Ignoring call to start cursor blinking since activity is not visible");
return;
}
// If cursor is to enabled now, then start cursor blinking if blinking is enabled
// otherwise stop cursor blinking
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
*/
public void onResetTerminalSession() {
// Ensure blinker starts again after reset if cursor blinking was disabled before reset like
// with "tput civis" which would have called onTerminalCursorStateChange()
mActivity.getTerminalView().setTerminalCursorBlinkerState(true, true);
}
@Override
public Integer getTerminalCursorStyle() {
return mActivity.getProperties().getTerminalCursorStyle();
}
/** Load mBellSoundPool */
private synchronized void loadBellSoundPool() {
if (mBellSoundPool == null) {
mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
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);
}
}
}
/** Release mBellSoundPool resources */
private synchronized void releaseBellSoundPool() {
if (mBellSoundPool != null) {
mBellSoundPool.release();
mBellSoundPool = null;
}
}
/** Try switching to session. */
@ -307,10 +157,8 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
void notifyOfSessionChange() {
if (!mActivity.isVisible()) return;
if (!mActivity.getProperties().areTerminalSessionChangeToastsDisabled()) {
TerminalSession session = mActivity.getCurrentSession();
mActivity.showToast(toToastTitle(session), false);
}
TerminalSession session = mActivity.getCurrentSession();
mActivity.showToast(toToastTitle(session), false);
}
public void switchToSession(boolean forward) {
@ -344,23 +192,12 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
public void renameSession(final TerminalSession sessionToRename) {
if (sessionToRename == null) return;
TextInputDialogUtils.textInput(mActivity, R.string.title_rename_session, sessionToRename.mSessionName, R.string.action_rename_session_confirm, text -> {
renameSession(sessionToRename, text);
DialogUtils.textInput(mActivity, R.string.title_rename_session, sessionToRename.mSessionName, R.string.action_rename_session_confirm, text -> {
sessionToRename.mSessionName = text;
termuxSessionListNotifyUpdated();
}, -1, null, -1, null, null);
}
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;

View File

@ -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;
}
}

View File

@ -2,54 +2,46 @@ package com.termux.app.terminal;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.os.Environment;
import android.net.Uri;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.Toast;
import com.termux.R;
import com.termux.app.TermuxActivity;
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.termux.TermuxConstants;
import com.termux.shared.activities.ReportActivity;
import com.termux.shared.models.ReportInfo;
import com.termux.app.activities.ReportActivity;
import com.termux.app.models.ReportInfo;
import com.termux.app.models.UserAction;
import com.termux.app.terminal.io.KeyboardShortcut;
import com.termux.shared.termux.settings.properties.TermuxPropertyConstants;
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
import com.termux.shared.settings.properties.TermuxPropertyConstants;
import com.termux.shared.data.DataUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.termux.TermuxUtils;
import com.termux.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.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,119 +49,20 @@ 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;
private Runnable mShowSoftKeyboardRunnable;
private boolean mShowSoftKeyboardIgnoreOnce;
private boolean mShowSoftKeyboardWithDelayOnce;
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() {
return mActivity;
}
/**
* Should be called when mActivity.onCreate() is called
*/
public void onCreate() {
onReloadProperties();
mActivity.getTerminalView().setTextSize(mActivity.getPreferences().getFontSize());
mActivity.getTerminalView().setKeepScreenOn(mActivity.getPreferences().shouldKeepScreenOn());
}
/**
* Should be called when mActivity.onStart() is called
*/
public void onStart() {
// Set {@link TerminalView#TERMINAL_VIEW_KEY_LOGGING_ENABLED} value
// Also required if user changed the preference from {@link TermuxSettings} activity and returns
boolean isTerminalViewKeyLoggingEnabled = mActivity.getPreferences().isTerminalViewKeyLoggingEnabled();
mActivity.getTerminalView().setIsTerminalViewKeyLoggingEnabled(isTerminalViewKeyLoggingEnabled);
// Piggyback on the terminal view key logging toggle for now, should add a separate toggle in future
mActivity.getTermuxActivityRootView().setIsRootViewLoggingEnabled(isTerminalViewKeyLoggingEnabled);
ViewUtils.setIsViewUtilsLoggingEnabled(isTerminalViewKeyLoggingEnabled);
}
/**
* Should be called when mActivity.onResume() is called
*/
public void onResume() {
// Show the soft keyboard if required
setSoftKeyboardState(true, mActivity.isActivityRecreated());
mTerminalCursorBlinkerStateAlreadySet = false;
if (mActivity.getTerminalView().mEmulator != null) {
// Start terminal cursor blinking if enabled
// If emulator is already set, then start blinker now, otherwise wait for onEmulatorSet()
// event to start it. This is needed since onEmulatorSet() may not be called after
// TermuxActivity is started after device display timeout with double tap and not power button.
setTerminalCursorBlinkerState(true);
mTerminalCursorBlinkerStateAlreadySet = true;
}
}
/**
* Should be called when mActivity.onStop() is called
*/
public void onStop() {
// Stop terminal cursor blinking if enabled
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() {
// Show the soft keyboard if required
setSoftKeyboardState(false, true);
// Start terminal cursor blinking if enabled
setTerminalCursorBlinkerState(true);
}
/**
* Should be called when {@link com.termux.view.TerminalView#mEmulator} is set
*/
@Override
public void onEmulatorSet() {
if (!mTerminalCursorBlinkerStateAlreadySet) {
// Start terminal cursor blinking if enabled
// We need to wait for the first session to be attached that's set in
// TermuxActivity.onServiceConnected() and then the multiple calls to TerminalView.updateSize()
// where the final one eventually sets the mEmulator when width/height is not 0. Otherwise
// blinker will not start again if TermuxActivity is started again after exiting it with
// double back press. Check TerminalView.setTerminalCursorBlinkerState().
setTerminalCursorBlinkerState(true);
mTerminalCursorBlinkerStateAlreadySet = true;
}
}
@Override
public float onScale(float scale) {
if (scale < 0.9f || scale > 1.1f) {
@ -184,26 +77,10 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
@Override
public void onSingleTapUp(MotionEvent e) {
TerminalEmulator term = mActivity.getCurrentSession().getEmulator();
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);
if (!urlSet.isEmpty()) {
String url = (String) urlSet.iterator().next();
ShareUtils.openUrl(mActivity, url);
return;
}
}
if (!term.isMouseTrackingActive() && !e.isFromSource(InputDevice.SOURCE_MOUSE)) {
if (!KeyboardUtils.areDisableSoftKeyboardFlagsSet(mActivity))
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
else
Logger.logVerbose(LOG_TAG, "Not showing soft keyboard onSingleTapUp since its disabled");
}
if (!KeyboardUtils.areDisableSoftKeyboardFlagsSet(mActivity))
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
else
Logger.logVerbose(LOG_TAG, "Not showing keyboard onSingleTapUp since its disabled");
}
@Override
@ -221,11 +98,6 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
return mActivity.getProperties().isUsingCtrlSpaceWorkaround();
}
@Override
public boolean isTerminalViewSelected() {
return mActivity.getTerminalToolbarViewPager() == null || mActivity.isTerminalViewSelected() || mActivity.getTerminalView().hasFocus();
}
@Override
@ -242,17 +114,16 @@ 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()) {
} else if (e.isCtrlPressed() && e.isAltPressed()) {
// Get the unmodified code point:
int unicodeChar = e.getUnicodeChar(0);
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || unicodeChar == 'n'/* next */) {
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 +133,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 +148,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;
}
@ -290,13 +161,6 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
@Override
public boolean onKeyUp(int keyCode, KeyEvent e) {
// If emulator is not set, like if bootstrap installation failed and user dismissed the error
// dialog, then just exit the activity, otherwise they will be stuck in a broken state.
if (keyCode == KeyEvent.KEYCODE_BACK && mActivity.getTerminalView().mEmulator == null) {
mActivity.finishActivityIfNotFinishing();
return true;
}
return handleVirtualKeys(keyCode, e, false);
}
@ -322,32 +186,12 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
@Override
public boolean readControlKey() {
return readExtraKeysSpecialButton(SpecialButton.CTRL) || mVirtualControlKeyDown;
return (mActivity.getExtraKeysView() != null && mActivity.getExtraKeysView().readSpecialButton(ExtraKeysView.SpecialButton.CTRL)) || mVirtualControlKeyDown;
}
@Override
public boolean readAltKey() {
return readExtraKeysSpecialButton(SpecialButton.ALT);
}
@Override
public boolean readShiftKey() {
return readExtraKeysSpecialButton(SpecialButton.SHIFT);
}
@Override
public boolean readFnKey() {
return readExtraKeysSpecialButton(SpecialButton.FN);
}
public boolean readExtraKeysSpecialButton(SpecialButton specialButton) {
if (mActivity.getExtraKeysView() == null) return false;
Boolean state = mActivity.getExtraKeysView().readSpecialButton(specialButton, true);
if (state == null) {
Logger.logError(LOG_TAG,"Failed to read an unregistered " + specialButton + " special button value from extra keys.");
return false;
}
return state;
return (mActivity.getExtraKeysView() != null && mActivity.getExtraKeysView().readSpecialButton(ExtraKeysView.SpecialButton.ALT));
}
@Override
@ -461,11 +305,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 +317,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 +337,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) {
@ -536,25 +359,16 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
mActivity.getPreferences().setSoftKeyboardEnabled(false);
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
} else {
// Show with a delay, otherwise pressing keyboard toggle won't show the keyboard after
// switching back from another app if keyboard was previously disabled by user.
// Also request focus, since it wouldn't have been requested at startup by
// setSoftKeyboardState if keyboard was disabled. #2112
Logger.logVerbose(LOG_TAG, "Enabling soft keyboard on toggle");
mActivity.getPreferences().setSoftKeyboardEnabled(true);
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
if(mShowSoftKeyboardWithDelayOnce) {
mShowSoftKeyboardWithDelayOnce = false;
mActivity.getTerminalView().postDelayed(getShowSoftKeyboardRunnable(), 500);
mActivity.getTerminalView().requestFocus();
} else
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
}
}
// If soft keyboard toggle behaviour is show/hide
else {
// If soft keyboard is disabled by user for Termux
if (!mActivity.getPreferences().isSoftKeyboardEnabled()) {
if (!mActivity.getPreferences().getSoftKeyboardEnabled()) {
Logger.logVerbose(LOG_TAG, "Maintaining disabled soft keyboard on toggle");
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
} else {
@ -566,31 +380,13 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
}
public void setSoftKeyboardState(boolean isStartup, boolean isReloadTermuxProperties) {
boolean noShowKeyboard = false;
// Requesting terminal view focus is necessary regardless of if soft keyboard is to be
// disabled or hidden at startup, otherwise if hardware keyboard is attached and user
// starts typing on hardware keyboard without tapping on the terminal first, then a colour
// tint will be added to the terminal as highlight for the focussed view. Test with a light
// theme. For android 8.+, the "defaultFocusHighlightEnabled" attribute is also set to false
// in TerminalView layout to fix the issue.
// If soft keyboard is disabled by user for Termux (check function docs for Termux behaviour info)
if (KeyboardUtils.shouldSoftKeyboardBeDisabled(mActivity,
mActivity.getPreferences().isSoftKeyboardEnabled(),
mActivity.getPreferences().isSoftKeyboardEnabledOnlyIfNoHardware())) {
// If soft keyboard is disabled by user for Termux
if (!mActivity.getPreferences().getSoftKeyboardEnabled()) {
Logger.logVerbose(LOG_TAG, "Maintaining disabled soft keyboard");
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
mActivity.getTerminalView().requestFocus();
noShowKeyboard = true;
// Delay is only required if onCreate() is called like when Termux app is exited with
// double back press, not when Termux app is switched back from another app and keyboard
// toggle is pressed to enable keyboard
if (isStartup && mActivity.isOnResumeAfterOnCreate())
mShowSoftKeyboardWithDelayOnce = true;
} else {
// Set flag to automatically push up TerminalView when keyboard is opened instead of showing over it
KeyboardUtils.setSoftInputModeAdjustResize(mActivity);
KeyboardUtils.setResizeTerminalViewForSoftKeyboardFlags(mActivity);
// Clear any previous flags to disable soft keyboard in case setting updated
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
@ -598,74 +394,32 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
// If soft keyboard is to be hidden on startup
if (isStartup && mActivity.getProperties().shouldSoftKeyboardBeHiddenOnStartup()) {
Logger.logVerbose(LOG_TAG, "Hiding soft keyboard on startup");
KeyboardUtils.hideSoftKeyboard(mActivity, mActivity.getTerminalView());
// Required to keep keyboard hidden when Termux app is switched back from another app
KeyboardUtils.setSoftKeyboardAlwaysHiddenFlags(mActivity);
} else {
// Do not force show soft keyboard if termux-reload-settings command was run with hardware keyboard
if (isReloadTermuxProperties)
return;
KeyboardUtils.hideSoftKeyboard(mActivity, mActivity.getTerminalView());
mActivity.getTerminalView().requestFocus();
noShowKeyboard = true;
// Required to keep keyboard hidden on app startup
mShowSoftKeyboardIgnoreOnce = true;
}
}
mActivity.getTerminalView().setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View view, boolean hasFocus) {
// Force show soft keyboard if TerminalView or toolbar text input view has
// focus and close it if they don't
boolean textInputViewHasFocus = false;
final EditText textInputView = mActivity.findViewById(R.id.terminal_toolbar_text_input);
if (textInputView != null) textInputViewHasFocus = textInputView.hasFocus();
if (hasFocus || textInputViewHasFocus) {
if (mShowSoftKeyboardIgnoreOnce) {
mShowSoftKeyboardIgnoreOnce = false; return;
}
Logger.logVerbose(LOG_TAG, "Showing soft keyboard on focus change");
} else {
Logger.logVerbose(LOG_TAG, "Hiding soft keyboard on focus change");
if (mShowSoftKeyboardRunnable == null) {
mShowSoftKeyboardRunnable = () -> {
Logger.logVerbose(LOG_TAG, "Showing soft keyboard on focus change");
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
};
}
KeyboardUtils.setSoftKeyboardVisibility(getShowSoftKeyboardRunnable(), mActivity, mActivity.getTerminalView(), hasFocus || textInputViewHasFocus);
mActivity.getTerminalView().setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View view, boolean hasFocus) {
// Force show soft keyboard if TerminalView has focus and close it if it doesn't
KeyboardUtils.setSoftKeyboardVisibility(mShowSoftKeyboardRunnable, mActivity, mActivity.getTerminalView(), hasFocus);
}
});
// Request focus for TerminalView
mActivity.getTerminalView().requestFocus();
}
});
// Do not force show soft keyboard if termux-reload-settings command was run with hardware keyboard
// or soft keyboard is to be hidden or is disabled
if (!isReloadTermuxProperties && !noShowKeyboard) {
// Request focus for TerminalView
// Also show the keyboard, since onFocusChange will not be called if TerminalView already
// had focus on startup to show the keyboard, like when opening url with context menu
// "Select URL" long press and returning to Termux app with back button. This
// will also show keyboard even if it was closed before opening url. #2111
Logger.logVerbose(LOG_TAG, "Requesting TerminalView focus and showing soft keyboard");
mActivity.getTerminalView().requestFocus();
mActivity.getTerminalView().postDelayed(getShowSoftKeyboardRunnable(), 300);
}
}
private Runnable getShowSoftKeyboardRunnable() {
if (mShowSoftKeyboardRunnable == null) {
mShowSoftKeyboardRunnable = () -> {
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
};
}
return mShowSoftKeyboardRunnable;
}
public void setTerminalCursorBlinkerState(boolean start) {
if (start) {
// If set/update the cursor blinking rate is successful, then enable cursor blinker
if (mActivity.getTerminalView().setTerminalCursorBlinkerRate(mActivity.getProperties().getTerminalCursorBlinkRate()))
mActivity.getTerminalView().setTerminalCursorBlinkerState(true, true);
else
Logger.logError(LOG_TAG,"Failed to start cursor blinker");
} else {
// Disable cursor blinker
mActivity.getTerminalView().setTerminalCursorBlinkerState(false, true);
}
}
@ -678,17 +432,17 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
if (transcriptText == null) return;
// See https://github.com/termux/termux-app/issues/1166.
transcriptText = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
ShareUtils.shareText(mActivity, mActivity.getString(R.string.title_share_transcript),
transcriptText, mActivity.getString(R.string.title_share_transcript_with));
}
public void shareSelectedText() {
String selectedText = mActivity.getTerminalView().getStoredSelectedText();
if (DataUtils.isNullOrEmpty(selectedText)) return;
ShareUtils.shareText(mActivity, mActivity.getString(R.string.title_share_selected_text),
selectedText, mActivity.getString(R.string.title_share_selected_text_with));
try {
// See https://github.com/termux/termux-app/issues/1166.
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
transcriptText = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
intent.putExtra(Intent.EXTRA_TEXT, transcriptText);
intent.putExtra(Intent.EXTRA_SUBJECT, mActivity.getString(R.string.title_share_transcript));
mActivity.startActivity(Intent.createChooser(intent, mActivity.getString(R.string.title_share_transcript_with)));
} catch (Exception e) {
Logger.logStackTraceWithMessage("Failed to get share session transcript of length " + transcriptText.length(), e);
}
}
public void showUrlSelection() {
@ -697,7 +451,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
String text = ShellUtils.getTerminalSessionTranscriptText(session, true, true);
LinkedHashSet<CharSequence> urlSet = TermuxUrlUtils.extractUrls(text);
LinkedHashSet<CharSequence> urlSet = DataUtils.extractUrls(text);
if (urlSet.isEmpty()) {
new AlertDialog.Builder(mActivity).setMessage(R.string.title_select_url_none_found).show();
return;
@ -709,7 +463,9 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
// Click to copy url to clipboard:
final AlertDialog dialog = new AlertDialog.Builder(mActivity).setItems(urls, (di, which) -> {
String url = (String) urls[which];
ShareUtils.copyTextToClipboard(mActivity, url, mActivity.getString(R.string.msg_select_url_copied_to_clipboard));
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(url)));
Toast.makeText(mActivity, R.string.msg_select_url_copied_to_clipboard, Toast.LENGTH_LONG).show();
}).setTitle(R.string.title_select_url_dialog).create();
// Long press to open URL:
@ -718,7 +474,13 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
lv.setOnItemLongClickListener((parent, view, position, id) -> {
dialog.dismiss();
String url = (String) urls[position];
ShareUtils.openUrl(mActivity, url);
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
try {
mActivity.startActivity(i, null);
} catch (ActivityNotFoundException e) {
// If no applications match, Android displays a system message.
mActivity.startActivity(Intent.createChooser(i, null));
}
return true;
});
});
@ -730,63 +492,26 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
TerminalSession session = mActivity.getCurrentSession();
if (session == null) return;
final String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
if (transcriptText == null) return;
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),
null);
}
transcriptText = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
private void reportIssueFromTranscript(String transcriptText, boolean addTermuxDebugInfo) {
Logger.showToast(mActivity, mActivity.getString(R.string.msg_generating_report), true);
StringBuilder reportString = new StringBuilder();
new Thread() {
@Override
public void run() {
StringBuilder reportString = new StringBuilder();
String title = TermuxConstants.TERMUX_APP_NAME + " Report Issue";
String title = TermuxConstants.TERMUX_APP_NAME + " Report Issue";
reportString.append("## Transcript\n");
reportString.append("\n").append(MarkdownUtils.getMarkdownCodeForString(transcriptText, true));
reportString.append("## Transcript\n");
reportString.append("\n").append(MarkdownUtils.getMarkdownCodeForString(transcriptText, true));
reportString.append("\n##\n");
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, true));
reportString.append("\n\n").append(TermuxUtils.getDeviceInfoMarkdownString(mActivity));
if (addTermuxDebugInfo) {
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGES));
} else {
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, TermuxUtils.AppInfoMode.TERMUX_PACKAGE));
}
String termuxAptInfo = TermuxUtils.geAPTInfoMarkdownString(mActivity);
if (termuxAptInfo != null)
reportString.append("\n\n").append(termuxAptInfo);
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(mActivity, true));
if (TermuxBootstrap.isAppPackageManagerAPT()) {
String termuxAptInfo = TermuxUtils.geAPTInfoMarkdownString(mActivity);
if (termuxAptInfo != null)
reportString.append("\n\n").append(termuxAptInfo);
}
if (addTermuxDebugInfo) {
String termuxDebugInfo = TermuxUtils.getTermuxDebugMarkdownString(mActivity);
if (termuxDebugInfo != null)
reportString.append("\n\n").append(termuxDebugInfo);
}
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);
}
}.start();
ReportActivity.startReportActivity(mActivity, new ReportInfo(UserAction.REPORT_ISSUE_FROM_TRANSCRIPT, TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY_NAME, title, null, reportString.toString(), "\n\n" + TermuxUtils.getReportIssueMarkdownString(mActivity), false));
}
public void doPaste() {
@ -794,9 +519,12 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
if (session == null) return;
if (!session.isRunning()) return;
String text = ShareUtils.getTextStringFromClipboardIfSet(mActivity, true);
if (text != null)
session.getEmulator().paste(text);
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboard.getPrimaryClip();
if (clipData == null) return;
CharSequence paste = clipData.getItemAt(0).coerceToText(mActivity);
if (!TextUtils.isEmpty(paste))
session.getEmulator().paste(paste.toString());
}
}

View File

@ -1,21 +1,15 @@
package com.termux.shared.termux.terminal.io;
package com.termux.app.terminal.io;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.os.VibrationEffect;
import android.os.Vibrator;
import com.termux.shared.logger.Logger;
public class BellHandler {
private static BellHandler instance = null;
private static final Object lock = new Object();
private static final String LOG_TAG = "BellHandler";
public static BellHandler getInstance(Context context) {
if (instance == null) {
synchronized (lock) {
@ -40,17 +34,7 @@ public class BellHandler {
@Override
public void run() {
if (vibrator != null) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(VibrationEffect.createOneShot(DURATION, VibrationEffect.DEFAULT_AMPLITUDE));
} else {
vibrator.vibrate(DURATION);
}
} catch (Exception e) {
// Issue on samsung devices on android 8
// java.lang.NullPointerException: Attempt to read from field 'android.os.VibrationEffect com.android.server.VibratorService$Vibration.mEffect' on a null object reference
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to run vibrator", e);
}
vibrator.vibrate(DURATION);
}
}
};

View File

@ -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.app.terminal.io.extrakeys.ExtraKeysView;
import com.termux.terminal.TerminalSession;
public class TerminalToolbarViewPager {
@ -44,11 +44,9 @@ 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.setButtonTextAllCaps(mActivity.getProperties().shouldExtraKeysTextBeAllCaps());
extraKeysView.setTermuxTerminalViewClient(mActivity.getTermuxTerminalViewClient());
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()) {

View File

@ -1,108 +0,0 @@
package com.termux.app.terminal.io;
import android.annotation.SuppressLint;
import android.view.Gravity;
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.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.view.TerminalView;
import org.json.JSONException;
public class TermuxTerminalExtraKeys extends TerminalExtraKeys {
private ExtraKeysInfo mExtraKeysInfo;
final TermuxActivity mActivity;
final TermuxTerminalViewClient mTermuxTerminalViewClient;
final TermuxTerminalSessionActivityClient mTermuxTerminalSessionActivityClient;
private static final String LOG_TAG = "TermuxTerminalExtraKeys";
public TermuxTerminalExtraKeys(TermuxActivity activity, @NonNull TerminalView terminalView,
TermuxTerminalViewClient termuxTerminalViewClient,
TermuxTerminalSessionActivityClient termuxTerminalSessionActivityClient) {
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;
}
@SuppressLint("RtlHardcoded")
@Override
public void onTerminalExtraKeyButtonClick(View view, String key, boolean ctrlDown, boolean altDown, boolean shiftDown, boolean fnDown) {
if ("KEYBOARD".equals(key)) {
if(mTermuxTerminalViewClient != null)
mTermuxTerminalViewClient.onToggleSoftKeyboardRequest();
} else if ("DRAWER".equals(key)) {
DrawerLayout drawerLayout = mTermuxTerminalViewClient.getActivity().getDrawer();
if (drawerLayout.isDrawerOpen(Gravity.LEFT))
drawerLayout.closeDrawer(Gravity.LEFT);
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();
} else {
super.onTerminalExtraKeyButtonClick(view, key, ctrlDown, altDown, shiftDown, fnDown);
}
}
}

View File

@ -0,0 +1,92 @@
package com.termux.app.terminal.io.extrakeys;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Arrays;
import java.util.stream.Collectors;
public class ExtraKeyButton {
/**
* The key that will be sent to the terminal, either a control character
* defined in ExtraKeysView.keyCodesForString (LEFT, RIGHT, PGUP...) or
* some text.
*/
private final String key;
/**
* If the key is a macro, i.e. a sequence of keys separated by space.
*/
private final boolean macro;
/**
* The text that will be shown on the button.
*/
private final String display;
/**
* The information of the popup (triggered by swipe up).
*/
@Nullable
private ExtraKeyButton popup;
public ExtraKeyButton(ExtraKeysInfo.CharDisplayMap charDisplayMap, JSONObject config) throws JSONException {
this(charDisplayMap, config, null);
}
public ExtraKeyButton(ExtraKeysInfo.CharDisplayMap charDisplayMap, JSONObject config, @Nullable ExtraKeyButton popup) throws JSONException {
String keyFromConfig = config.optString("key", null);
String macroFromConfig = config.optString("macro", null);
String[] keys;
if (keyFromConfig != null && macroFromConfig != null) {
throw new JSONException("Both key and macro can't be set for the same key");
} else if (keyFromConfig != null) {
keys = new String[]{keyFromConfig};
this.macro = false;
} else if (macroFromConfig != null) {
keys = macroFromConfig.split(" ");
this.macro = true;
} else {
throw new JSONException("All keys have to specify either key or macro");
}
for (int i = 0; i < keys.length; i++) {
keys[i] = ExtraKeysInfo.replaceAlias(keys[i]);
}
this.key = TextUtils.join(" ", keys);
String displayFromConfig = config.optString("display", null);
if (displayFromConfig != null) {
this.display = displayFromConfig;
} else {
this.display = Arrays.stream(keys)
.map(key -> charDisplayMap.get(key, key))
.collect(Collectors.joining(" "));
}
this.popup = popup;
}
public String getKey() {
return key;
}
public boolean isMacro() {
return macro;
}
public String getDisplay() {
return display;
}
@Nullable
public ExtraKeyButton getPopup() {
return popup;
}
}

View File

@ -0,0 +1,253 @@
package com.termux.app.terminal.io.extrakeys;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashMap;
public class ExtraKeysInfo {
/**
* Matrix of buttons displayed
*/
private final ExtraKeyButton[][] buttons;
/**
* This corresponds to one of the CharMapDisplay below
*/
private String style;
public ExtraKeysInfo(String propertiesInfo, String style) throws JSONException {
this.style = style;
// Convert String propertiesInfo to Array of Arrays
JSONArray arr = new JSONArray(propertiesInfo);
Object[][] matrix = new Object[arr.length()][];
for (int i = 0; i < arr.length(); i++) {
JSONArray line = arr.getJSONArray(i);
matrix[i] = new Object[line.length()];
for (int j = 0; j < line.length(); j++) {
matrix[i][j] = line.get(j);
}
}
// convert matrix to buttons
this.buttons = new ExtraKeyButton[matrix.length][];
for (int i = 0; i < matrix.length; i++) {
this.buttons[i] = new ExtraKeyButton[matrix[i].length];
for (int j = 0; j < matrix[i].length; j++) {
Object key = matrix[i][j];
JSONObject jobject = normalizeKeyConfig(key);
ExtraKeyButton button;
if (! jobject.has("popup")) {
// no popup
button = new ExtraKeyButton(getSelectedCharMap(), jobject);
} else {
// a popup
JSONObject popupJobject = normalizeKeyConfig(jobject.get("popup"));
ExtraKeyButton popup = new ExtraKeyButton(getSelectedCharMap(), popupJobject);
button = new ExtraKeyButton(getSelectedCharMap(), jobject, popup);
}
this.buttons[i][j] = button;
}
}
}
/**
* "hello" -> {"key": "hello"}
*/
private static JSONObject normalizeKeyConfig(Object key) throws JSONException {
JSONObject jobject;
if (key instanceof String) {
jobject = new JSONObject();
jobject.put("key", key);
} else if (key instanceof JSONObject) {
jobject = (JSONObject) key;
} else {
throw new JSONException("An key in the extra-key matrix must be a string or an object");
}
return jobject;
}
public ExtraKeyButton[][] getMatrix() {
return buttons;
}
/**
* HashMap that implements Python dict.get(key, default) function.
* Default java.util .get(key) is then the same as .get(key, null);
*/
static class CleverMap<K,V> extends HashMap<K,V> {
V get(K key, V defaultValue) {
if (containsKey(key))
return get(key);
else
return defaultValue;
}
}
static class CharDisplayMap extends CleverMap<String, String> {}
/**
* Keys are displayed in a natural looking way, like "" for "RIGHT"
*/
static final CharDisplayMap classicArrowsDisplay = new CharDisplayMap() {{
// classic arrow keys (for @see arrowVariationDisplay)
put("LEFT", ""); // U+2190 LEFTWARDS ARROW
put("RIGHT", ""); // U+2192 RIGHTWARDS ARROW
put("UP", ""); // U+2191 UPWARDS ARROW
put("DOWN", ""); // U+2193 DOWNWARDS ARROW
}};
static final CharDisplayMap wellKnownCharactersDisplay = new CharDisplayMap() {{
// well known characters // https://en.wikipedia.org/wiki/{Enter_key, Tab_key, Delete_key}
put("ENTER", ""); // U+21B2 DOWNWARDS ARROW WITH TIP LEFTWARDS
put("TAB", ""); // U+21B9 LEFTWARDS ARROW TO BAR OVER RIGHTWARDS ARROW TO BAR
put("BKSP", ""); // U+232B ERASE TO THE LEFT sometimes seen and easy to understand
put("DEL", ""); // U+2326 ERASE TO THE RIGHT not well known but easy to understand
put("DRAWER", ""); // U+2630 TRIGRAM FOR HEAVEN not well known but easy to understand
put("KEYBOARD", ""); // U+2328 KEYBOARD not well known but easy to understand
}};
static final CharDisplayMap lessKnownCharactersDisplay = new CharDisplayMap() {{
// https://en.wikipedia.org/wiki/{Home_key, End_key, Page_Up_and_Page_Down_keys}
// home key can mean "goto the beginning of line" or "goto first page" depending on context, hence the diagonal
put("HOME", ""); // from IEC 9995 // U+21F1 NORTH WEST ARROW TO CORNER
put("END", ""); // from IEC 9995 // // U+21F2 SOUTH EAST ARROW TO CORNER
put("PGUP", ""); // no ISO character exists, U+21D1 UPWARDS DOUBLE ARROW will do the trick
put("PGDN", ""); // no ISO character exists, U+21D3 DOWNWARDS DOUBLE ARROW will do the trick
}};
static final CharDisplayMap arrowTriangleVariationDisplay = new CharDisplayMap() {{
// alternative to classic arrow keys
put("LEFT", ""); // U+25C0 BLACK LEFT-POINTING TRIANGLE
put("RIGHT", ""); // U+25B6 BLACK RIGHT-POINTING TRIANGLE
put("UP", ""); // U+25B2 BLACK UP-POINTING TRIANGLE
put("DOWN", ""); // U+25BC BLACK DOWN-POINTING TRIANGLE
}};
static final CharDisplayMap notKnownIsoCharacters = new CharDisplayMap() {{
// Control chars that are more clear as text // https://en.wikipedia.org/wiki/{Function_key, Alt_key, Control_key, Esc_key}
// put("FN", "FN"); // no ISO character exists
put("CTRL", ""); // ISO character "U+2388 ⎈ HELM SYMBOL" is unknown to people and never printed on computers, however "U+25C7 ◇ WHITE DIAMOND" is a nice presentation, and "^" for terminal app and mac is often used
put("ALT", ""); // ISO character "U+2387 ⎇ ALTERNATIVE KEY SYMBOL'" is unknown to people and only printed as the Option key "" on Mac computer
put("ESC", ""); // ISO character "U+238B ⎋ BROKEN CIRCLE WITH NORTHWEST ARROW" is unknown to people and not often printed on computers
}};
static final CharDisplayMap nicerLookingDisplay = new CharDisplayMap() {{
// nicer looking for most cases
put("-", ""); // U+2015 HORIZONTAL BAR
}};
/*
* Multiple maps are available to quickly change
* the style of the keys.
*/
/**
* Some classic symbols everybody knows
*/
private static final CharDisplayMap defaultCharDisplay = new CharDisplayMap() {{
putAll(classicArrowsDisplay);
putAll(wellKnownCharactersDisplay);
putAll(nicerLookingDisplay);
// all other characters are displayed as themselves
}};
/**
* Classic symbols and less known symbols
*/
private static final CharDisplayMap lotsOfArrowsCharDisplay = new CharDisplayMap() {{
putAll(classicArrowsDisplay);
putAll(wellKnownCharactersDisplay);
putAll(lessKnownCharactersDisplay); // NEW
putAll(nicerLookingDisplay);
}};
/**
* Only arrows
*/
private static final CharDisplayMap arrowsOnlyCharDisplay = new CharDisplayMap() {{
putAll(classicArrowsDisplay);
// putAll(wellKnownCharactersDisplay); // REMOVED
// putAll(lessKnownCharactersDisplay); // REMOVED
putAll(nicerLookingDisplay);
}};
/**
* Full Iso
*/
private static final CharDisplayMap fullIsoCharDisplay = new CharDisplayMap() {{
putAll(classicArrowsDisplay);
putAll(wellKnownCharactersDisplay);
putAll(lessKnownCharactersDisplay); // NEW
putAll(nicerLookingDisplay);
putAll(notKnownIsoCharacters); // NEW
}};
/**
* Some people might call our keys differently
*/
static private final CharDisplayMap controlCharsAliases = new CharDisplayMap() {{
put("ESCAPE", "ESC");
put("CONTROL", "CTRL");
put("RETURN", "ENTER"); // Technically different keys, but most applications won't see the difference
put("FUNCTION", "FN");
// no alias for ALT
// Directions are sometimes written as first and last letter for brevety
put("LT", "LEFT");
put("RT", "RIGHT");
put("DN", "DOWN");
// put("UP", "UP"); well, "UP" is already two letters
put("PAGEUP", "PGUP");
put("PAGE_UP", "PGUP");
put("PAGE UP", "PGUP");
put("PAGE-UP", "PGUP");
// no alias for HOME
// no alias for END
put("PAGEDOWN", "PGDN");
put("PAGE_DOWN", "PGDN");
put("PAGE-DOWN", "PGDN");
put("DELETE", "DEL");
put("BACKSPACE", "BKSP");
// easier for writing in termux.properties
put("BACKSLASH", "\\");
put("QUOTE", "\"");
put("APOSTROPHE", "'");
}};
CharDisplayMap getSelectedCharMap() {
switch (style) {
case "arrows-only":
return arrowsOnlyCharDisplay;
case "arrows-all":
return lotsOfArrowsCharDisplay;
case "all":
return fullIsoCharDisplay;
case "none":
return new CharDisplayMap();
default:
return defaultCharDisplay;
}
}
/**
* Applies the 'controlCharsAliases' mapping to all the strings in *buttons*
* Modifies the array, doesn't return a new one.
*/
public static String replaceAlias(String key) {
return controlCharsAliases.get(key, key);
}
}

View File

@ -0,0 +1,388 @@
package com.termux.app.terminal.io.extrakeys;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.provider.Settings;
import android.util.AttributeSet;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ScheduledExecutorService;
import java.util.Map;
import java.util.HashMap;
import java.util.Arrays;
import java.util.stream.Collectors;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.GridLayout;
import android.widget.PopupWindow;
import com.termux.R;
import com.termux.app.terminal.TermuxTerminalViewClient;
import com.termux.view.TerminalView;
import androidx.drawerlayout.widget.DrawerLayout;
/**
* A view showing extra keys (such as Escape, Ctrl, Alt) not normally available on an Android soft
* keyboard.
*/
public final class ExtraKeysView extends GridLayout {
private static final int TEXT_COLOR = 0xFFFFFFFF;
private static final int BUTTON_COLOR = 0x00000000;
private static final int INTERESTING_COLOR = 0xFF80DEEA;
private static final int BUTTON_PRESSED_COLOR = 0xFF7F7F7F;
TermuxTerminalViewClient mTermuxTerminalViewClient;
public ExtraKeysView(Context context, AttributeSet attrs) {
super(context, attrs);
}
static final Map<String, Integer> keyCodesForString = new HashMap<String, Integer>() {{
put("SPACE", KeyEvent.KEYCODE_SPACE);
put("ESC", KeyEvent.KEYCODE_ESCAPE);
put("TAB", KeyEvent.KEYCODE_TAB);
put("HOME", KeyEvent.KEYCODE_MOVE_HOME);
put("END", KeyEvent.KEYCODE_MOVE_END);
put("PGUP", KeyEvent.KEYCODE_PAGE_UP);
put("PGDN", KeyEvent.KEYCODE_PAGE_DOWN);
put("INS", KeyEvent.KEYCODE_INSERT);
put("DEL", KeyEvent.KEYCODE_FORWARD_DEL);
put("BKSP", KeyEvent.KEYCODE_DEL);
put("UP", KeyEvent.KEYCODE_DPAD_UP);
put("LEFT", KeyEvent.KEYCODE_DPAD_LEFT);
put("RIGHT", KeyEvent.KEYCODE_DPAD_RIGHT);
put("DOWN", KeyEvent.KEYCODE_DPAD_DOWN);
put("ENTER", KeyEvent.KEYCODE_ENTER);
put("F1", KeyEvent.KEYCODE_F1);
put("F2", KeyEvent.KEYCODE_F2);
put("F3", KeyEvent.KEYCODE_F3);
put("F4", KeyEvent.KEYCODE_F4);
put("F5", KeyEvent.KEYCODE_F5);
put("F6", KeyEvent.KEYCODE_F6);
put("F7", KeyEvent.KEYCODE_F7);
put("F8", KeyEvent.KEYCODE_F8);
put("F9", KeyEvent.KEYCODE_F9);
put("F10", KeyEvent.KEYCODE_F10);
put("F11", KeyEvent.KEYCODE_F11);
put("F12", KeyEvent.KEYCODE_F12);
}};
@SuppressLint("RtlHardcoded")
private void sendKey(View view, String keyName, boolean forceCtrlDown, boolean forceLeftAltDown) {
TerminalView terminalView = view.findViewById(R.id.terminal_view);
if ("KEYBOARD".equals(keyName)) {
if(mTermuxTerminalViewClient != null)
mTermuxTerminalViewClient.onToggleSoftKeyboardRequest();
} else if ("DRAWER".equals(keyName)) {
DrawerLayout drawer = view.findViewById(R.id.drawer_layout);
drawer.openDrawer(Gravity.LEFT);
} else if (keyCodesForString.containsKey(keyName)) {
Integer keyCode = keyCodesForString.get(keyName);
if (keyCode == null) return;
int metaState = 0;
if (forceCtrlDown) {
metaState |= KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON;
}
if (forceLeftAltDown) {
metaState |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON;
}
KeyEvent keyEvent = new KeyEvent(0, 0, KeyEvent.ACTION_UP, keyCode, 0, metaState);
terminalView.onKeyDown(keyCode, keyEvent);
} else {
// not a control char
keyName.codePoints().forEach(codePoint -> {
terminalView.inputCodePoint(codePoint, forceCtrlDown, forceLeftAltDown);
});
}
}
private void sendKey(View view, ExtraKeyButton buttonInfo) {
if (buttonInfo.isMacro()) {
String[] keys = buttonInfo.getKey().split(" ");
boolean ctrlDown = false;
boolean altDown = false;
for (String key : keys) {
if ("CTRL".equals(key)) {
ctrlDown = true;
} else if ("ALT".equals(key)) {
altDown = true;
} else {
sendKey(view, key, ctrlDown, altDown);
ctrlDown = false;
altDown = false;
}
}
} else {
sendKey(view, buttonInfo.getKey(), false, false);
}
}
public enum SpecialButton {
CTRL, ALT, FN
}
private static class SpecialButtonState {
boolean isOn = false;
boolean isActive = false;
List<Button> buttons = new ArrayList<>();
void setIsActive(boolean value) {
isActive = value;
buttons.forEach(button -> button.setTextColor(value ? INTERESTING_COLOR : TEXT_COLOR));
}
}
private final Map<SpecialButton, SpecialButtonState> specialButtons = new HashMap<SpecialButton, SpecialButtonState>() {{
put(SpecialButton.CTRL, new SpecialButtonState());
put(SpecialButton.ALT, new SpecialButtonState());
put(SpecialButton.FN, new SpecialButtonState());
}};
private final Set<String> specialButtonsKeys = specialButtons.keySet().stream().map(Enum::name).collect(Collectors.toSet());
private boolean isSpecialButton(ExtraKeyButton button) {
return specialButtonsKeys.contains(button.getKey());
}
private ScheduledExecutorService scheduledExecutor;
private PopupWindow popupWindow;
private int longPressCount;
public boolean readSpecialButton(SpecialButton name) {
SpecialButtonState state = specialButtons.get(name);
if (state == null)
throw new RuntimeException("Must be a valid special button (see source)");
if (!state.isOn || !state.isActive)
return false;
state.setIsActive(false);
return true;
}
private Button createSpecialButton(String buttonKey, boolean needUpdate) {
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonKey));
if (state == null) return null;
state.isOn = true;
Button button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
button.setTextColor(state.isActive ? INTERESTING_COLOR : TEXT_COLOR);
if (needUpdate) {
state.buttons.add(button);
}
return button;
}
void popup(View view, ExtraKeyButton extraButton) {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
Button button;
if (isSpecialButton(extraButton)) {
button = createSpecialButton(extraButton.getKey(), false);
if (button == null) return;
} else {
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
button.setTextColor(TEXT_COLOR);
}
button.setText(extraButton.getDisplay());
button.setPadding(0, 0, 0, 0);
button.setMinHeight(0);
button.setMinWidth(0);
button.setMinimumWidth(0);
button.setMinimumHeight(0);
button.setWidth(width);
button.setHeight(height);
button.setBackgroundColor(BUTTON_PRESSED_COLOR);
popupWindow = new PopupWindow(this);
popupWindow.setWidth(LayoutParams.WRAP_CONTENT);
popupWindow.setHeight(LayoutParams.WRAP_CONTENT);
popupWindow.setContentView(button);
popupWindow.setOutsideTouchable(true);
popupWindow.setFocusable(false);
popupWindow.showAsDropDown(view, 0, -2 * height);
}
/**
* General util function to compute the longest column length in a matrix.
*/
static int maximumLength(Object[][] matrix) {
int m = 0;
for (Object[] row : matrix)
m = Math.max(m, row.length);
return m;
}
/**
* Reload the view given parameters in termux.properties
*
* @param infos matrix as defined in termux.properties extrakeys
* Can Contain The Strings CTRL ALT TAB FN ENTER LEFT RIGHT UP DOWN or normal strings
* Some aliases are possible like RETURN for ENTER, LT for LEFT and more (@see controlCharsAliases for the whole list).
* Any string of length > 1 in total Uppercase will print a warning
*
* Examples:
* "ENTER" will trigger the ENTER keycode
* "LEFT" will trigger the LEFT keycode and be displayed as ""
* "" will input a "" character
* "" will input a "" character
* "-_-" will input the string "-_-"
*/
@SuppressLint("ClickableViewAccessibility")
public void reload(ExtraKeysInfo infos) {
if (infos == null)
return;
for(SpecialButtonState state : specialButtons.values())
state.buttons = new ArrayList<>();
removeAllViews();
ExtraKeyButton[][] buttons = infos.getMatrix();
setRowCount(buttons.length);
setColumnCount(maximumLength(buttons));
for (int row = 0; row < buttons.length; row++) {
for (int col = 0; col < buttons[row].length; col++) {
final ExtraKeyButton buttonInfo = buttons[row][col];
Button button;
if (isSpecialButton(buttonInfo)) {
button = createSpecialButton(buttonInfo.getKey(), true);
if (button == null) return;
} else {
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
}
button.setText(buttonInfo.getDisplay());
button.setTextColor(TEXT_COLOR);
button.setPadding(0, 0, 0, 0);
final Button finalButton = button;
button.setOnClickListener(v -> {
if (Settings.System.getInt(getContext().getContentResolver(),
Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) != 0) {
if (Build.VERSION.SDK_INT >= 28) {
finalButton.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
} else {
// Perform haptic feedback only if no total silence mode enabled.
if (Settings.Global.getInt(getContext().getContentResolver(), "zen_mode", 0) != 2) {
finalButton.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
}
}
}
View root = getRootView();
if (isSpecialButton(buttonInfo)) {
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonInfo.getKey()));
if (state == null) return;
state.setIsActive(!state.isActive);
} else {
sendKey(root, buttonInfo);
}
});
button.setOnTouchListener((v, event) -> {
final View root = getRootView();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
longPressCount = 0;
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
if (Arrays.asList("UP", "DOWN", "LEFT", "RIGHT", "BKSP", "DEL").contains(buttonInfo.getKey())) {
// autorepeat
scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
scheduledExecutor.scheduleWithFixedDelay(() -> {
longPressCount++;
sendKey(root, buttonInfo);
}, 400, 80, TimeUnit.MILLISECONDS);
}
return true;
case MotionEvent.ACTION_MOVE:
if (buttonInfo.getPopup() != null) {
if (popupWindow == null && event.getY() < 0) {
if (scheduledExecutor != null) {
scheduledExecutor.shutdownNow();
scheduledExecutor = null;
}
v.setBackgroundColor(BUTTON_COLOR);
popup(v, buttonInfo.getPopup());
}
if (popupWindow != null && event.getY() > 0) {
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
popupWindow.dismiss();
popupWindow = null;
}
}
return true;
case MotionEvent.ACTION_CANCEL:
v.setBackgroundColor(BUTTON_COLOR);
if (scheduledExecutor != null) {
scheduledExecutor.shutdownNow();
scheduledExecutor = null;
}
return true;
case MotionEvent.ACTION_UP:
v.setBackgroundColor(BUTTON_COLOR);
if (scheduledExecutor != null) {
scheduledExecutor.shutdownNow();
scheduledExecutor = null;
}
if (longPressCount == 0 || popupWindow != null) {
if (popupWindow != null) {
popupWindow.setContentView(null);
popupWindow.dismiss();
popupWindow = null;
if (buttonInfo.getPopup() != null) {
if (isSpecialButton(buttonInfo.getPopup())) {
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonInfo.getPopup().getKey()));
if (state == null) return true;
state.setIsActive(!state.isActive);
} else {
sendKey(root, buttonInfo.getPopup());
}
}
} else {
v.performClick();
}
}
return true;
default:
return true;
}
});
LayoutParams param = new GridLayout.LayoutParams();
param.width = 0;
param.height = 0;
param.setMargins(0, 0, 0, 0);
param.columnSpec = GridLayout.spec(col, GridLayout.FILL, 1.f);
param.rowSpec = GridLayout.spec(row, GridLayout.FILL, 1.f);
button.setLayoutParams(param);
addView(button);
}
}
}
public void setTermuxTerminalViewClient(TermuxTerminalViewClient termuxTerminalViewClient) {
this.mTermuxTerminalViewClient = termuxTerminalViewClient;
}
}

View File

@ -0,0 +1,156 @@
package com.termux.app.utils;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import androidx.annotation.Nullable;
import com.termux.R;
import com.termux.app.activities.ReportActivity;
import com.termux.shared.notification.NotificationUtils;
import com.termux.shared.file.FileUtils;
import com.termux.app.models.ReportInfo;
import com.termux.app.models.UserAction;
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
import com.termux.shared.settings.preferences.TermuxPreferenceConstants;
import com.termux.shared.data.DataUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.termux.TermuxUtils;
import com.termux.shared.termux.TermuxConstants;
import java.nio.charset.Charset;
public class CrashUtils {
private static final String LOG_TAG = "CrashUtils";
/**
* Notify the user of a previous app crash by reading the crash info from the crash log file at
* {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}. The crash log file would have been
* created by {@link com.termux.shared.crash.CrashHandler}.
*
* If the crash log file exists and is not empty and
* {@link TermuxPreferenceConstants.TERMUX_APP#KEY_CRASH_REPORT_NOTIFICATIONS_ENABLED} is
* enabled, then a notification will be shown for the crash on the
* {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME} channel, otherwise nothing will be done.
*
* After reading from the crash log file, it will be moved to {@link TermuxConstants#TERMUX_CRASH_LOG_BACKUP_FILE_PATH}.
*
* @param context The {@link Context} for operations.
* @param logTagParam The log tag to use for logging.
*/
public static void notifyCrash(final Context context, final String logTagParam) {
if (context == null) return;
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(context);
// If user has disabled notifications for crashes
if (!preferences.getCrashReportNotificationsEnabled())
return;
new Thread() {
@Override
public void run() {
String logTag = DataUtils.getDefaultIfNull(logTagParam, LOG_TAG);
if (!FileUtils.regularFileExists(TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, false))
return;
String errmsg;
StringBuilder reportStringBuilder = new StringBuilder();
// Read report string from crash log file
errmsg = FileUtils.readStringFromFile(context, "crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, Charset.defaultCharset(), reportStringBuilder, false);
if (errmsg != null) {
Logger.logError(logTag, errmsg);
return;
}
// Move crash log file to backup location if it exists
FileUtils.moveRegularFile(context, "crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, TermuxConstants.TERMUX_CRASH_LOG_BACKUP_FILE_PATH, true);
if (errmsg != null) {
Logger.logError(logTag, errmsg);
}
String reportString = reportStringBuilder.toString();
if (reportString == null || reportString.isEmpty())
return;
// Send a notification to show the crash log which when clicked will open the {@link ReportActivity}
// to show the details of the crash
String title = TermuxConstants.TERMUX_APP_NAME + " Crash Report";
Logger.logDebug(logTag, "The crash log file at \"" + TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH + "\" found. Sending \"" + title + "\" notification.");
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.CRASH_REPORT, logTag, title, null, reportString, "\n\n" + TermuxUtils.getReportIssueMarkdownString(context), true));
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
// Setup the notification channel if not already set up
setupCrashReportsNotificationChannel(context);
// Build the notification
Notification.Builder builder = getCrashReportsNotificationBuilder(context, title, null, null, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
if (builder == null) return;
// Send the notification
int nextNotificationId = NotificationUtils.getNextNotificationId(context);
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
if (notificationManager != null)
notificationManager.notify(nextNotificationId, builder.build());
}
}.start();
}
/**
* Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID}
* and {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}.
*
* @param context The {@link Context} for operations.
* @param title The title for the notification.
* @param notificationText The second line text of the notification.
* @param notificationBigText The full text of the notification that may optionally be styled.
* @param pendingIntent The {@link PendingIntent} which should be sent when notification is clicked.
* @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}.
* @return Returns the {@link Notification.Builder}.
*/
@Nullable
public static Notification.Builder getCrashReportsNotificationBuilder(final Context context, final CharSequence title, final CharSequence notificationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) {
Notification.Builder builder = NotificationUtils.geNotificationBuilder(context,
TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_HIGH,
title, notificationText, notificationBigText, pendingIntent, notificationMode);
if (builder == null) return null;
// Enable timestamp
builder.setShowWhen(true);
// Set notification icon
builder.setSmallIcon(R.drawable.ic_error_notification);
// Set background color for small notification icon
builder.setColor(0xFF607D8B);
// Dismiss on click
builder.setAutoCancel(true);
return builder;
}
/**
* Setup the notification channel for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID} and
* {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}.
*
* @param context The {@link Context} for operations.
*/
public static void setupCrashReportsNotificationChannel(final Context context) {
NotificationUtils.setupNotificationChannel(context, TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID,
TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
}
}

View File

@ -0,0 +1,330 @@
package com.termux.app.utils;
import android.app.Activity;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import com.termux.R;
import com.termux.shared.notification.NotificationUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
import com.termux.app.activities.ReportActivity;
import com.termux.shared.logger.Logger;
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_APP;
import com.termux.shared.settings.properties.SharedProperties;
import com.termux.shared.settings.properties.TermuxPropertyConstants;
import com.termux.app.models.ReportInfo;
import com.termux.shared.models.ExecutionCommand;
import com.termux.app.models.UserAction;
import com.termux.shared.data.DataUtils;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.termux.TermuxUtils;
public class PluginUtils {
/** Required file permissions for the executable file of execute intent. Executable file must have read and execute permissions */
public static final String PLUGIN_EXECUTABLE_FILE_PERMISSIONS = "r-x"; // Default: "r-x"
/** Required file permissions for the working directory of execute intent. Working directory must have read and write permissions.
* Execute permissions should be attempted to be set, but ignored if they are missing */
public static final String PLUGIN_WORKING_DIRECTORY_PERMISSIONS = "rwx"; // Default: "rwx"
private static final String LOG_TAG = "PluginUtils";
/**
* Process {@link ExecutionCommand} result.
*
* The ExecutionCommand currentState must be greater or equal to
* {@link ExecutionCommand.ExecutionState#EXECUTED}.
* If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and
* {@link ExecutionCommand#pluginPendingIntent} is not {@code null}, then the result of commands
* are sent back to the {@link PendingIntent} creator.
*
* @param context The {@link Context} that will be used to send result intent to the {@link PendingIntent} creator.
* @param logTag The log tag to use for logging.
* @param executionCommand The {@link ExecutionCommand} to process.
*/
public static void processPluginExecutionCommandResult(final Context context, String logTag, final ExecutionCommand executionCommand) {
if (executionCommand == null) return;
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
if (!executionCommand.hasExecuted()) {
Logger.logWarn(logTag, "Ignoring call to processPluginExecutionCommandResult() since the execution command state is not higher than the ExecutionState.EXECUTED");
return;
}
Logger.logDebug(LOG_TAG, executionCommand.toString());
boolean result = true;
// If isPluginExecutionCommand is true and pluginPendingIntent is not null, then
// send pluginPendingIntent to its creator with the result
if (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null) {
String errmsg = executionCommand.errmsg;
//Combine errmsg and stacktraces
if (executionCommand.isStateFailed()) {
errmsg = Logger.getMessageAndStackTracesString(executionCommand.errmsg, executionCommand.throwableList);
}
// Send pluginPendingIntent to its creator
result = sendPluginExecutionCommandResultPendingIntent(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.stdout, executionCommand.stderr, executionCommand.exitCode, executionCommand.errCode, errmsg, executionCommand.pluginPendingIntent);
}
if (!executionCommand.isStateFailed() && result)
executionCommand.setState(ExecutionCommand.ExecutionState.SUCCESS);
}
/**
* Process {@link ExecutionCommand} error.
*
* The ExecutionCommand currentState must be equal to {@link ExecutionCommand.ExecutionState#FAILED}.
* The {@link ExecutionCommand#errCode} must have been set to a value greater than
* {@link ExecutionCommand#RESULT_CODE_OK}.
* The {@link ExecutionCommand#errmsg} and any {@link ExecutionCommand#throwableList} must also
* be set with appropriate error info.
*
* If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and
* {@link ExecutionCommand#pluginPendingIntent} is not {@code null}, then the errors of commands
* are sent back to the {@link PendingIntent} creator.
*
* Otherwise if the {@link TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED} is
* enabled, then a flash and a notification will be shown for the error as well
* on the {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME} channel instead of just logging
* the error.
*
* @param context The {@link Context} for operations.
* @param logTag The log tag to use for logging.
* @param executionCommand The {@link ExecutionCommand} that failed.
* @param forceNotification If set to {@code true}, then a flash and notification will be shown
* regardless of if pending intent is {@code null} or
* {@link TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED}
* is {@code false}.
*/
public static void processPluginExecutionCommandError(final Context context, String logTag, final ExecutionCommand executionCommand, boolean forceNotification) {
if (context == null || executionCommand == null) return;
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
if (!executionCommand.isStateFailed()) {
Logger.logWarn(logTag, "Ignoring call to processPluginExecutionCommandError() since the execution command is not in ExecutionState.FAILED");
return;
}
// Log the error and any exception
Logger.logStackTracesWithMessage(logTag, "(" + executionCommand.errCode + ") " + executionCommand.errmsg, executionCommand.throwableList);
// If isPluginExecutionCommand is true and pluginPendingIntent is not null, then
// send pluginPendingIntent to its creator with the errors
if (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null) {
String errmsg = executionCommand.errmsg;
//Combine errmsg and stacktraces
if (executionCommand.isStateFailed()) {
errmsg = Logger.getMessageAndStackTracesString(executionCommand.errmsg, executionCommand.throwableList);
}
sendPluginExecutionCommandResultPendingIntent(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.stdout, executionCommand.stderr, executionCommand.exitCode, executionCommand.errCode, errmsg, executionCommand.pluginPendingIntent);
// No need to show notifications if a pending intent was sent, let the caller handle the result himself
if (!forceNotification) return;
}
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(context);
// If user has disabled notifications for plugin, then just return
if (!preferences.getPluginErrorNotificationsEnabled() && !forceNotification)
return;
// Flash the errmsg
Logger.showToast(context, executionCommand.errmsg, true);
// Send a notification to show the errmsg which when clicked will open the {@link ReportActivity}
// to show the details of the error
String title = TermuxConstants.TERMUX_APP_NAME + " Plugin Execution Command Error";
StringBuilder reportString = new StringBuilder();
reportString.append(ExecutionCommand.getExecutionCommandMarkdownString(executionCommand));
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true));
reportString.append("\n\n").append(TermuxUtils.getDeviceInfoMarkdownString(context));
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.PLUGIN_EXECUTION_COMMAND, logTag, title, null, reportString.toString(), null,true));
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
// Setup the notification channel if not already set up
setupPluginCommandErrorsNotificationChannel(context);
// Use markdown in notification
CharSequence notificationText = MarkdownUtils.getSpannedMarkdownText(context, executionCommand.errmsg);
//CharSequence notificationText = executionCommand.errmsg;
// Build the notification
Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(context, title, notificationText, notificationText, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
if (builder == null) return;
// Send the notification
int nextNotificationId = NotificationUtils.getNextNotificationId(context);
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
if (notificationManager != null)
notificationManager.notify(nextNotificationId, builder.build());
}
/**
* Send {@link ExecutionCommand} result {@link PendingIntent} in the
* {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE} bundle.
*
*
* @param context The {@link Context} that will be used to send result intent to the {@link PendingIntent} creator.
* @param logTag The log tag to use for logging.
* @param label The label of {@link ExecutionCommand}.
* @param stdout The stdout of {@link ExecutionCommand}.
* @param stderr The stderr of {@link ExecutionCommand}.
* @param exitCode The exitCode of {@link ExecutionCommand}.
* @param errCode The errCode of {@link ExecutionCommand}.
* @param errmsg The errmsg of {@link ExecutionCommand}.
* @param pluginPendingIntent The pluginPendingIntent of {@link ExecutionCommand}.
* @return Returns {@code true} if pluginPendingIntent was successfully send, otherwise [@code false}.
*/
public static boolean sendPluginExecutionCommandResultPendingIntent(Context context, String logTag, String label, String stdout, String stderr, Integer exitCode, Integer errCode, String errmsg, PendingIntent pluginPendingIntent) {
if (context == null || pluginPendingIntent == null) return false;
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
Logger.logDebug(logTag, "Sending execution result for Execution Command \"" + label + "\" to " + pluginPendingIntent.getCreatorPackage());
String truncatedStdout = null;
String truncatedStderr = null;
String stdoutOriginalLength = (stdout == null) ? null: String.valueOf(stdout.length());
String stderrOriginalLength = (stderr == null) ? null: String.valueOf(stderr.length());
// Truncate stdout and stdout to max TRANSACTION_SIZE_LIMIT_IN_BYTES
if (stderr == null || stderr.isEmpty()) {
truncatedStdout = DataUtils.getTruncatedCommandOutput(stdout, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false);
} else if (stdout == null || stdout.isEmpty()) {
truncatedStderr = DataUtils.getTruncatedCommandOutput(stderr, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false);
} else {
truncatedStdout = DataUtils.getTruncatedCommandOutput(stdout, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 2, false, false, false);
truncatedStderr = DataUtils.getTruncatedCommandOutput(stderr, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 2, false, false, false);
}
if (truncatedStdout != null && truncatedStdout.length() < stdout.length()) {
Logger.logWarn(logTag, "Execution Result for Execution Command \"" + label + "\" stdout length truncated from " + stdoutOriginalLength + " to " + truncatedStdout.length());
stdout = truncatedStdout;
}
if (truncatedStderr != null && truncatedStderr.length() < stderr.length()) {
Logger.logWarn(logTag, "Execution Result for Execution Command \"" + label + "\" stderr length truncated from " + stderrOriginalLength + " to " + truncatedStderr.length());
stderr = truncatedStderr;
}
String errmsgOriginalLength = (errmsg == null) ? null: String.valueOf(errmsg.length());
// Truncate errmsg to max TRANSACTION_SIZE_LIMIT_IN_BYTES / 4
// trim from end to preserve start of stacktraces
String truncatedErrmsg = DataUtils.getTruncatedCommandOutput(errmsg, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 4, true, false, false);
if (truncatedErrmsg != null && truncatedErrmsg.length() < errmsg.length()) {
Logger.logWarn(logTag, "Execution Result for Execution Command \"" + label + "\" errmsg length truncated from " + errmsgOriginalLength + " to " + truncatedErrmsg.length());
errmsg = truncatedErrmsg;
}
final Bundle resultBundle = new Bundle();
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT, stdout);
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH, stdoutOriginalLength);
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR, stderr);
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH, stderrOriginalLength);
if (exitCode != null) resultBundle.putInt(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE, exitCode);
if (errCode != null) resultBundle.putInt(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERR, errCode);
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG, errmsg);
Intent resultIntent = new Intent();
resultIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE, resultBundle);
try {
pluginPendingIntent.send(context, Activity.RESULT_OK, resultIntent);
} catch (PendingIntent.CanceledException e) {
// The caller doesn't want the result? That's fine, just ignore
Logger.logDebug(logTag, "The Execution Command \"" + label + "\" creator " + pluginPendingIntent.getCreatorPackage() + " does not want the results anymore");
}
return true;
}
/**
* Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
* and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
*
* @param context The {@link Context} for operations.
* @param title The title for the notification.
* @param notificationText The second line text of the notification.
* @param notificationBigText The full text of the notification that may optionally be styled.
* @param pendingIntent The {@link PendingIntent} which should be sent when notification is clicked.
* @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}.
* @return Returns the {@link Notification.Builder}.
*/
@Nullable
public static Notification.Builder getPluginCommandErrorsNotificationBuilder(final Context context, final CharSequence title, final CharSequence notificationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) {
Notification.Builder builder = NotificationUtils.geNotificationBuilder(context,
TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_HIGH,
title, notificationText, notificationBigText, pendingIntent, notificationMode);
if (builder == null) return null;
// Enable timestamp
builder.setShowWhen(true);
// Set notification icon
builder.setSmallIcon(R.drawable.ic_error_notification);
// Set background color for small notification icon
builder.setColor(0xFF607D8B);
// Dismiss on click
builder.setAutoCancel(true);
return builder;
}
/**
* Setup the notification channel for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID} and
* {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
*
* @param context The {@link Context} for operations.
*/
public static void setupPluginCommandErrorsNotificationChannel(final Context context) {
NotificationUtils.setupNotificationChannel(context, TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID,
TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
}
/**
* Check if {@link TermuxConstants#PROP_ALLOW_EXTERNAL_APPS} property is not set to "true".
*
* @param context The {@link Context} to get error string.
* @return Returns the {@code errmsg} if policy is violated, otherwise {@code null}.
*/
public static String checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(final Context context) {
String errmsg = null;
if (!SharedProperties.isPropertyValueTrue(context, TermuxPropertyConstants.getTermuxPropertiesFile(), TermuxConstants.PROP_ALLOW_EXTERNAL_APPS)) {
errmsg = context.getString(R.string.error_run_command_service_allow_external_apps_ungranted);
}
return errmsg;
}
}

View File

@ -0,0 +1,202 @@
package com.termux.filepicker;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.provider.OpenableColumns;
import android.util.Patterns;
import com.termux.R;
import com.termux.shared.interact.DialogUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
import com.termux.app.TermuxService;
import com.termux.shared.logger.Logger;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.regex.Pattern;
public class TermuxFileReceiverActivity extends Activity {
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";
static final String URL_OPENER_PROGRAM = TermuxConstants.TERMUX_HOME_DIR_PATH + "/bin/termux-url-opener";
/**
* If the activity should be finished when the name input dialog is dismissed. This is disabled
* before showing an error dialog, since the act of showing the error dialog will cause the
* name input dialog to be implicitly dismissed, and we do not want to finish the activity directly
* when showing the error dialog.
*/
boolean mFinishOnDismissNameDialog = true;
private static final String LOG_TAG = "TermuxFileReceiverActivity";
static boolean isSharedTextAnUrl(String sharedText) {
return Patterns.WEB_URL.matcher(sharedText).matches()
|| Pattern.matches("magnet:\\?xt=urn:btih:.*?", sharedText);
}
@Override
protected void onResume() {
super.onResume();
final Intent intent = getIntent();
final String action = intent.getAction();
final String type = intent.getType();
final String scheme = intent.getScheme();
if (Intent.ACTION_SEND.equals(action) && type != null) {
final String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
final Uri sharedUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
if (sharedText != null) {
if (isSharedTextAnUrl(sharedText)) {
handleUrlAndFinish(sharedText);
} else {
String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
if (subject == null) subject = intent.getStringExtra(Intent.EXTRA_TITLE);
if (subject != null) subject += ".txt";
promptNameAndSave(new ByteArrayInputStream(sharedText.getBytes(StandardCharsets.UTF_8)), subject);
}
} else if (sharedUri != null) {
handleContentUri(sharedUri, intent.getStringExtra(Intent.EXTRA_TITLE));
} else {
showErrorDialogAndQuit("Send action without content - nothing to save.");
}
} else if ("content".equals(scheme)) {
handleContentUri(intent.getData(), intent.getStringExtra(Intent.EXTRA_TITLE));
} else if ("file".equals(scheme)) {
// When e.g. clicking on a downloaded apk:
String path = intent.getData().getPath();
File file = new File(path);
try {
FileInputStream in = new FileInputStream(file);
promptNameAndSave(in, file.getName());
} catch (FileNotFoundException e) {
showErrorDialogAndQuit("Cannot open file: " + e.getMessage() + ".");
}
} else {
showErrorDialogAndQuit("Unable to receive any file or URL.");
}
}
void showErrorDialogAndQuit(String message) {
mFinishOnDismissNameDialog = false;
new AlertDialog.Builder(this).setMessage(message).setOnDismissListener(dialog -> finish()).setPositiveButton(android.R.string.ok, (dialog, which) -> finish()).show();
}
void handleContentUri(final Uri uri, String subjectFromIntent) {
try {
String attachmentFileName = null;
String[] projection = new String[]{OpenableColumns.DISPLAY_NAME};
try (Cursor c = getContentResolver().query(uri, projection, null, null, null)) {
if (c != null && c.moveToFirst()) {
final int fileNameColumnId = c.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (fileNameColumnId >= 0) attachmentFileName = c.getString(fileNameColumnId);
}
}
if (attachmentFileName == null) attachmentFileName = subjectFromIntent;
InputStream in = getContentResolver().openInputStream(uri);
promptNameAndSave(in, attachmentFileName);
} catch (Exception e) {
showErrorDialogAndQuit("Unable to handle shared content:\n\n" + e.getMessage());
Logger.logStackTraceWithMessage(LOG_TAG, "handleContentUri(uri=" + uri + ") failed", e);
}
}
void promptNameAndSave(final InputStream in, final String attachmentFileName) {
DialogUtils.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;
}
// Do this for the user if necessary:
//noinspection ResultOfMethodCallIgnored
editorProgramFile.setExecutable(true);
final Uri scriptUri = new Uri.Builder().scheme("file").path(EDITOR_PROGRAM).build();
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(TermuxFileReceiverActivity.this, TermuxService.class);
startService(executeIntent);
finish();
},
android.R.string.cancel, text -> finish(), dialog -> {
if (mFinishOnDismissNameDialog) finish();
});
}
public File saveStreamWithName(InputStream in, String attachmentFileName) {
File receiveDir = new File(TERMUX_RECEIVEDIR);
if (!receiveDir.isDirectory() && !receiveDir.mkdirs()) {
showErrorDialogAndQuit("Cannot create directory: " + receiveDir.getAbsolutePath());
return null;
}
try {
final File outFile = new File(receiveDir, attachmentFileName);
try (FileOutputStream f = new FileOutputStream(outFile)) {
byte[] buffer = new byte[4096];
int readBytes;
while ((readBytes = in.read(buffer)) > 0) {
f.write(buffer, 0, readBytes);
}
}
return outFile;
} catch (IOException e) {
showErrorDialogAndQuit("Error saving file:\n\n" + e);
Logger.logStackTraceWithMessage(LOG_TAG, "Error saving file", e);
return null;
}
}
void handleUrlAndFinish(final String url) {
final File urlOpenerProgramFile = new File(URL_OPENER_PROGRAM);
if (!urlOpenerProgramFile.isFile()) {
showErrorDialogAndQuit("The following file does not exist:\n$HOME/bin/termux-url-opener\n\n"
+ "Create this file as a script or a symlink - it will be called with the shared URL as only argument.");
return;
}
// Do this for the user if necessary:
//noinspection ResultOfMethodCallIgnored
urlOpenerProgramFile.setExecutable(true);
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(TermuxFileReceiverActivity.this, TermuxService.class);
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, new String[]{url});
startService(executeIntent);
finish();
}
}

View File

@ -5,8 +5,8 @@
android:orientation="vertical">
<include
layout="@layout/partial_primary_toolbar"
android:id="@+id/partial_primary_toolbar"/>
layout="@layout/partial_toolbar"
android:id="@+id/partial_toolbar"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"

View File

@ -21,8 +21,6 @@
android:paddingRight="16dip"
android:paddingBottom="8dip"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?android:attr/textColorPrimary"
android:textColorLink="?android:attr/textColorLink"
android:textSize="12sp" />
</HorizontalScrollView>

View File

@ -11,6 +11,5 @@
android:paddingTop="8dip"
android:paddingBottom="8dip"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?android:attr/textColorPrimary"
android:textColorLink="?android:attr/textColorLink"
android:textColor="#000"
android:textSize="12sp" />

View File

@ -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>

View File

@ -1,113 +1,80 @@
<com.termux.app.terminal.TermuxActivityRootView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_termux_root_view"
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:fitsSystemWindows="true">
<RelativeLayout
android:id="@+id/activity_termux_root_relative_layout"
<androidx.drawerlayout.widget.DrawerLayout
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginHorizontal="3dp"
android:layout_marginVertical="0dp"
android:orientation="vertical">
android:layout_alignParentTop="true"
android:layout_above="@+id/terminal_toolbar_view_pager"
android:layout_height="match_parent">
<androidx.drawerlayout.widget.DrawerLayout
android:id="@+id/drawer_layout"
<com.termux.view.TerminalView
android:id="@+id/terminal_view"
android:layout_width="match_parent"
android:layout_alignParentTop="true"
android:layout_above="@+id/terminal_toolbar_view_pager"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:layout_marginRight="3dp"
android:layout_marginLeft="3dp"
android:focusableInTouchMode="true"
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
android:scrollbars="vertical"
android:importantForAutofill="no"
android:autofillHints="password" />
<com.termux.view.TerminalView
android:id="@+id/terminal_view"
<LinearLayout
android:id="@+id/left_drawer"
android:layout_width="240dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="@android:color/white"
android:choiceMode="singleChoice"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"
android:descendantFocusability="blocksDescendants"
android:orientation="vertical">
<ListView
android:id="@+id/terminal_sessions_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:defaultFocusHighlightEnabled="false"
android:focusableInTouchMode="true"
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
android:scrollbars="vertical"
tools:ignore="UnusedAttribute" />
android:layout_height="0dp"
android:layout_gravity="top"
android:layout_weight="1"
android:choiceMode="singleChoice"
android:longClickable="true" />
<LinearLayout
android:id="@+id/left_drawer"
android:layout_width="240dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:choiceMode="singleChoice"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"
android:descendantFocusability="blocksDescendants"
android:orientation="vertical"
android:background="?attr/termuxActivityDrawerBackground">
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<LinearLayout
<Button
android:id="@+id/toggle_keyboard_button"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageButton
android:id="@+id/settings_button"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_settings"
android:background="@null"
android:contentDescription="@string/action_open_settings"
app:tint="?attr/termuxActivityDrawerImageTint" />
</LinearLayout>
<ListView
android:id="@+id/terminal_sessions_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_gravity="top"
android:layout_weight="1"
android:choiceMode="singleChoice"
android:longClickable="true" />
android:text="@string/action_toggle_soft_keyboard" />
<LinearLayout
style="?android:attr/buttonBarStyle"
<Button
android:id="@+id/new_session_button"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/toggle_keyboard_button"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/action_toggle_soft_keyboard" />
<com.google.android.material.button.MaterialButton
android:id="@+id/new_session_button"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/action_new_session" />
</LinearLayout>
android:layout_weight="1"
android:text="@string/action_new_session" />
</LinearLayout>
</LinearLayout>
</androidx.drawerlayout.widget.DrawerLayout>
</androidx.drawerlayout.widget.DrawerLayout>
<androidx.viewpager.widget.ViewPager
android:id="@+id/terminal_toolbar_view_pager"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="37.5dp"
android:background="@color/black"
android:layout_alignParentBottom="true" />
</RelativeLayout>
<View
android:id="@+id/activity_termux_bottom_space_view"
<androidx.viewpager.widget.ViewPager
android:id="@+id/terminal_toolbar_view_pager"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@android:color/transparent" />
</com.termux.app.terminal.TermuxActivityRootView>
android:layout_height="37.5dp"
android:background="@android:drawable/screen_background_dark_transparent"
android:layout_alignParentBottom="true" />
</RelativeLayout>

View File

@ -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"

View File

@ -13,10 +13,10 @@
android:background="?attr/colorPrimaryDark"
android:gravity="center_vertical"
android:minHeight="?attr/actionBarSize"
android:theme="@style/ThemeOverlay.AppCompat.DayNight.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.DayNight"
app:titleTextAppearance="@style/TextAppearance.Widget.BaseToolbar.Title"
app:subtitleTextAppearance="@style/TextAppearance.Widget.BaseToolbar.Subtitle">
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:titleTextAppearance="@style/Toolbar.Title">
</androidx.appcompat.widget.Toolbar>
</LinearLayout>

View File

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-appcompat-release/preference/preference/res/layout/preference.xml
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="vertical">
<TextView android:id="@android:id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="?android:attr/textColorPrimary" />
<include android:id="@android:id/summary" layout="@layout/markdown_adapter_node_default" />
</LinearLayout>

View File

@ -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.app.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"

View File

@ -12,9 +12,4 @@
android:icon="@drawable/ic_copy"
android:title="@string/action_copy"
app:showAsAction="never" />
<item
android:id="@+id/menu_item_save_report_to_file"
android:title="@string/action_save_to_file"
app:showAsAction="never" />
</menu>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="termuxActivityDrawerBackground" format="reference" />
<attr name="termuxActivityDrawerImageTint" format="reference" />
</resources>

View File

@ -4,8 +4,7 @@
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="content_padding_half">4dip</dimen>
<dimen name="content_padding">8dip</dimen>
<dimen name="content_padding_double">16dip</dimen>
<dimen name="content_padding_triple">32dip</dimen>
<dimen name="content_padding_half">4dip</dimen>
</resources>

View File

@ -9,10 +9,10 @@
<!ENTITY TERMUX_STYLING_APP_NAME "Termux:Styling">
<!ENTITY TERMUX_TASKER_APP_NAME "Termux:Tasker">
<!ENTITY TERMUX_WIDGET_APP_NAME "Termux:Widget">
<!ENTITY TERMUX_PROPERTIES_PRIMARY_PATH_SHORT "~/.termux/termux.properties">
]>
<resources>
<string name="application_name">&TERMUX_APP_NAME;</string>
<string name="shared_user_label">&TERMUX_APP_NAME; user</string>
@ -21,7 +21,7 @@
<!-- Termux RUN_COMMAND permission -->
<string name="permission_run_command_label">Run commands in &TERMUX_APP_NAME; environment</string>
<string name="permission_run_command_description">execute arbitrary commands within &TERMUX_APP_NAME;
environment and access files</string>
environment</string>
@ -31,13 +31,7 @@
<string name="bootstrap_error_body">&TERMUX_APP_NAME; was unable to install the bootstrap packages.</string>
<string name="bootstrap_error_abort">Abort</string>
<string name="bootstrap_error_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>
<string name="bootstrap_error_not_primary_user_message">&TERMUX_APP_NAME; can only be run as the primary user.\nBootstrap binaries compiled for &TERMUX_APP_NAME; have hardcoded $PREFIX path and cannot be installed under any path other than \"%1$s\".</string>
@ -69,15 +63,10 @@
<string name="title_share_transcript">Terminal transcript</string>
<string name="title_share_transcript_with">Send transcript to:</string>
<string name="action_share_selected_text">Share selected text</string>
<string name="title_share_selected_text">Terminal Text</string>
<string name="title_share_selected_text_with">Send selected text to:</string>
<string name="action_autofill_username">Autofill username</string>
<string name="action_autofill_password">Autofill password</string>
<string name="action_reset_terminal">Reset</string>
<string name="msg_terminal_reset">Terminal reset</string>
<string name="msg_terminal_reset">Terminal reset.</string>
<string name="action_kill_process">Kill process (%d)</string>
<string name="title_confirm_kill_process">Really kill this session?</string>
@ -86,10 +75,7 @@
<string name="action_toggle_keep_screen_on">Keep screen on</string>
<string name="action_open_help">Help</string>
<string name="action_open_settings">Settings</string>
<string name="action_report_issue">Report Issue</string>
<string name="msg_generating_report">Generating Report</string>
<string name="msg_add_termux_debug_info">Add termux debug info to report?</string>
<string name="error_styling_not_installed">The &TERMUX_STYLING_APP_NAME; Plugin App is not installed.</string>
<string name="action_styling_install">Install</string>
@ -103,25 +89,29 @@
<!-- TermuxService -->
<string name="error_display_over_other_apps_permission_not_granted_to_start_terminal">&TERMUX_APP_NAME; requires
\"Display over other apps\" permission to start terminal sessions from background on Android >= 10.
Grants it from Settings -> Apps -> &TERMUX_APP_NAME; -> Advanced</string>
<string name="error_termux_service_invalid_execution_command_runner">Invalid execution command runner to TermuxService: `%1$s`</string>
<string name="error_termux_service_unsupported_execution_command_runner">Unsupported execution command runner to TermuxService: `%1$s`</string>
<string name="error_termux_service_unsupported_execution_command_shell_create_mode">Unsupported execution command shell create mode to TermuxService: `%1$s`</string>
<string name="error_termux_service_execution_command_shell_name_unset">Shell name not set but `%1$s` shell create mode passed</string>
<!-- Termux RunCommandService -->
<string name="error_run_command_service_invalid_intent_action">Invalid intent action to RunCommandService: `%1$s`</string>
<string name="error_run_command_service_invalid_execution_command_runner">Invalid execution command runner to RunCommandService: `%1$s`</string>
<string name="error_run_command_service_mandatory_extra_missing">Mandatory extra missing to RunCommandService: \"%1$s\"</string>
<string name="error_run_command_service_allow_external_apps_ungranted">RunCommandService require `allow-external-apps` property to be set to `true` in `&TERMUX_PROPERTIES_PRIMARY_PATH_SHORT;` file.</string>
<string name="error_run_command_service_api_help">Visit %1$s for more info on RUN_COMMAND Intent usage.</string>
<!-- Termux Execution Commands -->
<string name="msg_executable_absolute_path">Executable Absolute Path: \"%1$s\"</string>
<string name="msg_working_directory_absolute_path">Working Directory Absolute Path: \"%1$s\"</string>
<!-- Termux Report And ShareUtils -->
<string name="action_copy">Copy</string>
<string name="action_share">Share</string>
<string name="title_share_with">Share With</string>
<string name="title_report_text">Report Text</string>
<!-- Termux File Receiver -->
<string name="title_file_received">Save file in ~/downloads/</string>
<string name="action_file_received_edit">Edit</string>
@ -129,110 +119,40 @@
<!-- 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>
<!-- Termux Settings -->
<string name="title_activity_termux_settings">&TERMUX_APP_NAME; Settings</string>
<!-- Termux App Preferences -->
<string name="termux_preferences_title">&TERMUX_APP_NAME;</string>
<string name="termux_preferences_summary">Preferences for &TERMUX_APP_NAME; app</string>
<!-- Debugging Preferences -->
<string name="debugging_preferences">Debugging</string>
<!-- Debugging Preferences -->
<string name="termux_debugging_preferences_title">Debugging</string>
<string name="termux_debugging_preferences_summary">Preferences for debugging</string>
<!-- Logging Category -->
<string name="logging_header">Logging</string>
<!-- Logging Category -->
<string name="termux_logging_header">Logging</string>
<!-- Terminal View Key Logging -->
<string name="terminal_view_key_logging_title">Terminal View Key Logging</string>
<string name="terminal_view_key_logging_off">Logs will not have entries for terminal view keys. (Default)</string>
<string name="terminal_view_key_logging_on">Logcat logs will have entries for terminal view keys. These are very verbose and should be disabled under normal circumstances or will cause performance issues.</string>
<!-- Log Level -->
<string name="termux_log_level_title">Log Level</string>
<!-- Plugin Error Notifications -->
<string name="plugin_error_notifications_title">Plugin Error Notifications</string>
<string name="plugin_error_notifications_off">Disable flashes and notifications for plugin errors.</string>
<string name="plugin_error_notifications_on">Show flashes and notifications for plugin errors. (Default)</string>
<!-- Terminal View Key Logging -->
<string name="termux_terminal_view_key_logging_enabled_title">Terminal View Key Logging</string>
<string name="termux_terminal_view_key_logging_enabled_off">Logs will not have entries for terminal view keys. (Default)</string>
<string name="termux_terminal_view_key_logging_enabled_on">Logcat logs will have entries for terminal view keys.
These are very verbose and should be disabled under normal circumstances or will cause performance issues.</string>
<!-- Plugin Error Notifications -->
<string name="termux_plugin_error_notifications_enabled_title">Plugin Error Notifications</string>
<string name="termux_plugin_error_notifications_enabled_off">Disable flashes and notifications for plugin errors.</string>
<string name="termux_plugin_error_notifications_enabled_on">Show flashes and notifications for plugin errors. (Default)</string>
<!-- Crash Report Notifications -->
<string name="termux_crash_report_notifications_enabled_title">Crash Report Notifications</string>
<string name="termux_crash_report_notifications_enabled_off">Disable notifications for crash reports.</string>
<string name="termux_crash_report_notifications_enabled_on">Show notifications for crash reports. (Default)</string>
<!-- Crash Report Notifications -->
<string name="crash_report_notifications_title">Crash Report Notifications</string>
<string name="crash_report_notifications_off">Disable notifications for crash reports.</string>
<string name="crash_report_notifications_on">Show notifications for crash reports. (Default)</string>
<!-- Terminal IO Preferences -->
<string name="termux_terminal_io_preferences_title">Terminal I/O</string>
<string name="termux_terminal_io_preferences_summary">Preferences for terminal I/O</string>
<!-- Terminal IO Preferences -->
<string name="terminal_io_preferences">Terminal I/O</string>
<!-- Keyboard Category -->
<string name="termux_keyboard_header">Keyboard</string>
<!-- Keyboard Category -->
<string name="keyboard_header">Keyboard</string>
<!-- Soft Keyboard -->
<string name="termux_soft_keyboard_enabled_title">Soft Keyboard Enabled</string>
<string name="termux_soft_keyboard_enabled_off">Soft keyboard will be disabled.</string>
<string name="termux_soft_keyboard_enabled_on">Soft keyboard will be enabled. (Default)</string>
<!-- Soft Keyboard Only If No Hardware-->
<string name="termux_soft_keyboard_enabled_only_if_no_hardware_title">Soft Keyboard Only If No Hardware</string>
<string name="termux_soft_keyboard_enabled_only_if_no_hardware_off">Soft keyboard will be enabled even if
hardware keyboard is connected. (Default)</string>
<string name="termux_soft_keyboard_enabled_only_if_no_hardware_on">Soft keyboard will be enabled only if
no hardware keyboard is connected.</string>
<!-- Terminal View Preferences -->
<string name="termux_terminal_view_preferences_title">Terminal View</string>
<string name="termux_terminal_view_preferences_summary">Preferences for terminal view</string>
<!-- View Category -->
<string name="termux_terminal_view_view_header">View</string>
<!-- Terminal View Margin Adjustment -->
<string name="termux_terminal_view_terminal_margin_adjustment_title">Terminal Margin Adjustment</string>
<string name="termux_terminal_view_terminal_margin_adjustment_off">Terminal margin adjustment will be disabled.</string>
<string name="termux_terminal_view_terminal_margin_adjustment_on">Terminal margin adjustment will be enabled.
It should be enabled to try to fix the issue where soft keyboard covers part of extra keys/terminal view.
If it causes screen flickering on your devices, then disable it. (Default)</string>
<!-- Termux:API App Preferences -->
<string name="termux_api_preferences_title">&TERMUX_API_APP_NAME;</string>
<string name="termux_api_preferences_summary">Preferences for &TERMUX_API_APP_NAME; app</string>
<!-- Termux:Float App Preferences -->
<string name="termux_float_preferences_title">&TERMUX_FLOAT_APP_NAME;</string>
<string name="termux_float_preferences_summary">Preferences for &TERMUX_FLOAT_APP_NAME; app</string>
<!-- Termux:Tasker App Preferences -->
<string name="termux_tasker_preferences_title">&TERMUX_TASKER_APP_NAME;</string>
<string name="termux_tasker_preferences_summary">Preferences for &TERMUX_TASKER_APP_NAME; app</string>
<!-- Termux:Widget App Preferences -->
<string name="termux_widget_preferences_title">&TERMUX_WIDGET_APP_NAME;</string>
<string name="termux_widget_preferences_summary">Preferences for &TERMUX_WIDGET_APP_NAME; app</string>
<!-- About Preference -->
<string name="about_preference_title">About</string>
<!-- Donate Preference -->
<string name="donate_preference_title">Donate</string>
<!-- Soft Keyboard -->
<string name="soft_keyboard_title">Soft Keyboard</string>
<string name="soft_keyboard_off">Soft keyboard will be disabled.</string>
<string name="soft_keyboard_on">Soft keyboard will be enabled. (Default)</string>
</resources>

View File

@ -1,17 +1,61 @@
<?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="Theme.AppCompat.TermuxReportActivity" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimaryDark">#FF0000</item>
</style>
<style name="Toolbar.Title" parent="TextAppearance.Widget.AppCompat.Toolbar.Title">
<item name="android:textSize">14sp</item>
</style>
<style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">
<!-- Seen in buttons on alert dialog: -->
<item name="android:colorAccent">#212121</item>
</style>
<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>

View File

@ -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>

View File

@ -0,0 +1,33 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:key="logging"
app:title="@string/logging_header">
<ListPreference
app:defaultValue="1"
app:key="log_level"
app:title="@string/log_level_title"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
app:key="terminal_view_key_logging_enabled"
app:summaryOff="@string/terminal_view_key_logging_off"
app:summaryOn="@string/terminal_view_key_logging_on"
app:title="@string/terminal_view_key_logging_title" />
<SwitchPreferenceCompat
app:key="plugin_error_notifications_enabled"
app:summaryOff="@string/plugin_error_notifications_off"
app:summaryOn="@string/plugin_error_notifications_on"
app:title="@string/plugin_error_notifications_title" />
<SwitchPreferenceCompat
app:key="crash_report_notifications_enabled"
app:summaryOff="@string/crash_report_notifications_off"
app:summaryOn="@string/crash_report_notifications_on"
app:title="@string/crash_report_notifications_title" />
</PreferenceCategory>
</PreferenceScreen>

View File

@ -1,49 +1,13 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
app:key="termux"
app:title="@string/termux_preferences_title"
app:summary="@string/termux_preferences_summary"
app:fragment="com.termux.app.fragments.settings.TermuxPreferencesFragment"/>
app:title="@string/debugging_preferences"
app:summary="Preferences for debugging"
app:fragment="com.termux.app.fragments.settings.DebuggingPreferencesFragment"/>
<Preference
app:key="termux_api"
app:title="@string/termux_api_preferences_title"
app:summary="@string/termux_api_preferences_summary"
app:isPreferenceVisible="false"
app:fragment="com.termux.app.fragments.settings.TermuxAPIPreferencesFragment"/>
<Preference
app:key="termux_float"
app:title="@string/termux_float_preferences_title"
app:summary="@string/termux_float_preferences_summary"
app:isPreferenceVisible="false"
app:fragment="com.termux.app.fragments.settings.TermuxFloatPreferencesFragment"/>
<Preference
app:key="termux_tasker"
app:title="@string/termux_tasker_preferences_title"
app:summary="@string/termux_tasker_preferences_summary"
app:isPreferenceVisible="false"
app:fragment="com.termux.app.fragments.settings.TermuxTaskerPreferencesFragment"/>
<Preference
app:key="termux_widget"
app:title="@string/termux_widget_preferences_title"
app:summary="@string/termux_widget_preferences_summary"
app:isPreferenceVisible="false"
app:fragment="com.termux.app.fragments.settings.TermuxWidgetPreferencesFragment"/>
<Preference
app:key="about"
app:title="@string/about_preference_title"
app:persistent="false"/>
<!-- app:layout="@layout/preference_markdown_text" -->
<Preference
app:key="donate"
app:title="@string/donate_preference_title"
app:persistent="false"
app:isPreferenceVisible="false"/>
app:title="@string/terminal_io_preferences"
app:summary="Preferences for terminal I/O"
app:fragment="com.termux.app.fragments.settings.TerminalIOPreferencesFragment"/>
</PreferenceScreen>

View File

@ -0,0 +1,15 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:key="keyboard"
app:title="@string/keyboard_header">
<SwitchPreferenceCompat
app:key="soft_keyboard_enabled"
app:summaryOff="@string/soft_keyboard_off"
app:summaryOn="@string/soft_keyboard_on"
app:title="@string/soft_keyboard_title" />
</PreferenceCategory>
</PreferenceScreen>

View File

@ -1,15 +0,0 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:key="logging"
app:title="@string/termux_logging_header">
<ListPreference
app:defaultValue="1"
app:key="log_level"
app:title="@string/termux_log_level_title"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
</PreferenceScreen>

View File

@ -1,8 +0,0 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
app:title="@string/termux_debugging_preferences_title"
app:summary="@string/termux_debugging_preferences_summary"
app:fragment="com.termux.app.fragments.settings.termux_api.DebuggingPreferencesFragment"/>
</PreferenceScreen>

View File

@ -1,33 +0,0 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:key="logging"
app:title="@string/termux_logging_header">
<ListPreference
app:defaultValue="1"
app:key="log_level"
app:title="@string/termux_log_level_title"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
app:key="terminal_view_key_logging_enabled"
app:summaryOff="@string/termux_terminal_view_key_logging_enabled_off"
app:summaryOn="@string/termux_terminal_view_key_logging_enabled_on"
app:title="@string/termux_terminal_view_key_logging_enabled_title" />
<SwitchPreferenceCompat
app:key="plugin_error_notifications_enabled"
app:summaryOff="@string/termux_plugin_error_notifications_enabled_off"
app:summaryOn="@string/termux_plugin_error_notifications_enabled_on"
app:title="@string/termux_plugin_error_notifications_enabled_title" />
<SwitchPreferenceCompat
app:key="crash_report_notifications_enabled"
app:summaryOff="@string/termux_crash_report_notifications_enabled_off"
app:summaryOn="@string/termux_crash_report_notifications_enabled_on"
app:title="@string/termux_crash_report_notifications_enabled_title" />
</PreferenceCategory>
</PreferenceScreen>

View File

@ -1,21 +0,0 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:key="logging"
app:title="@string/termux_logging_header">
<ListPreference
app:defaultValue="1"
app:key="log_level"
app:title="@string/termux_log_level_title"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
app:key="terminal_view_key_logging_enabled"
app:summaryOff="@string/termux_terminal_view_key_logging_enabled_off"
app:summaryOn="@string/termux_terminal_view_key_logging_enabled_on"
app:title="@string/termux_terminal_view_key_logging_enabled_title" />
</PreferenceCategory>
</PreferenceScreen>

View File

@ -1,8 +0,0 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
app:title="@string/termux_debugging_preferences_title"
app:summary="@string/termux_debugging_preferences_summary"
app:fragment="com.termux.app.fragments.settings.termux_float.DebuggingPreferencesFragment"/>
</PreferenceScreen>

View File

@ -1,18 +0,0 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
app:title="@string/termux_debugging_preferences_title"
app:summary="@string/termux_debugging_preferences_summary"
app:fragment="com.termux.app.fragments.settings.termux.DebuggingPreferencesFragment"/>
<Preference
app:title="@string/termux_terminal_io_preferences_title"
app:summary="@string/termux_terminal_io_preferences_summary"
app:fragment="com.termux.app.fragments.settings.termux.TerminalIOPreferencesFragment"/>
<Preference
app:title="@string/termux_terminal_view_preferences_title"
app:summary="@string/termux_terminal_view_preferences_summary"
app:fragment="com.termux.app.fragments.settings.termux.TerminalViewPreferencesFragment"/>
</PreferenceScreen>

View File

@ -1,15 +0,0 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:key="logging"
app:title="@string/termux_logging_header">
<ListPreference
app:defaultValue="1"
app:key="log_level"
app:title="@string/termux_log_level_title"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
</PreferenceScreen>

View File

@ -1,8 +0,0 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
app:title="@string/termux_debugging_preferences_title"
app:summary="@string/termux_debugging_preferences_summary"
app:fragment="com.termux.app.fragments.settings.termux_tasker.DebuggingPreferencesFragment"/>
</PreferenceScreen>

View File

@ -1,21 +0,0 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:key="keyboard"
app:title="@string/termux_keyboard_header">
<SwitchPreferenceCompat
app:key="soft_keyboard_enabled"
app:summaryOff="@string/termux_soft_keyboard_enabled_off"
app:summaryOn="@string/termux_soft_keyboard_enabled_on"
app:title="@string/termux_soft_keyboard_enabled_title" />
<SwitchPreferenceCompat
app:key="soft_keyboard_enabled_only_if_no_hardware"
app:summaryOff="@string/termux_soft_keyboard_enabled_only_if_no_hardware_off"
app:summaryOn="@string/termux_soft_keyboard_enabled_only_if_no_hardware_on"
app:title="@string/termux_soft_keyboard_enabled_only_if_no_hardware_title" />
</PreferenceCategory>
</PreferenceScreen>

View File

@ -1,15 +0,0 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:key="view"
app:title="@string/termux_terminal_view_view_header">
<SwitchPreferenceCompat
app:key="terminal_margin_adjustment"
app:summaryOff="@string/termux_terminal_view_terminal_margin_adjustment_off"
app:summaryOn="@string/termux_terminal_view_terminal_margin_adjustment_on"
app:title="@string/termux_terminal_view_terminal_margin_adjustment_title" />
</PreferenceCategory>
</PreferenceScreen>

View File

@ -1,15 +0,0 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:key="logging"
app:title="@string/termux_logging_header">
<ListPreference
app:defaultValue="1"
app:key="log_level"
app:title="@string/termux_log_level_title"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
</PreferenceScreen>

View File

@ -1,8 +0,0 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
app:title="@string/termux_debugging_preferences_title"
app:summary="@string/termux_debugging_preferences_summary"
app:fragment="com.termux.app.fragments.settings.termux_widget.DebuggingPreferencesFragment"/>
</PreferenceScreen>

View File

@ -1,6 +1,6 @@
package com.termux.app;
import com.termux.shared.termux.data.TermuxUrlUtils;
import com.termux.shared.data.DataUtils;
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, DataUtils.extractUrls(text));
}
@Test

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