diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a83ef38 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: +- package-ecosystem: github-actions + directory: / + schedule: + interval: daily diff --git a/.github/workflows/debug_build.yml b/.github/workflows/debug_build.yml deleted file mode 100644 index 59287cc..0000000 --- a/.github/workflows/debug_build.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Build - -on: push - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Clone repository - uses: actions/checkout@v2 - - name: Build - run: | - ./gradlew assembleDebug - - name: Store generated APK file - uses: actions/upload-artifact@v1 - with: - name: termux-tasker - path: ./app/build/outputs/apk/debug/app-debug.apk diff --git a/.github/workflows/github_action_build.yml b/.github/workflows/github_action_build.yml new file mode 100644 index 0000000..2699694 --- /dev/null +++ b/.github/workflows/github_action_build.yml @@ -0,0 +1,75 @@ +name: GitHub Action Build + +on: + push: + branches: + - master + pull_request: + branches: + - master + schedule: + - cron: "15 0 1 */2 *" + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Clone repository + uses: actions/checkout@v4 + + - name: Build + shell: bash {0} + 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_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 release version '${RELEASE_VERSION_NAME/v/}' generated from current version '$CURRENT_VERSION_NAME' 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.github.debug" # Note the ".", GITHUB_SHA will already have "+" before it + APK_BASENAME_PREFIX="termux-tasker-app_$APK_VERSION_TAG" + + # Used by upload step 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 APK file for '$RELEASE_VERSION_NAME' release with '$APK_VERSION_TAG' tag" + export TERMUX_TASKER_APP__BUILD__APP_VERSION_NAME="${RELEASE_VERSION_NAME/v/}" # Used by app/build.gradle + export TERMUX_TASKER_APP__BUILD__APK_VERSION_TAG="$APK_VERSION_TAG" # Used by app/build.gradle + if ! ./gradlew assembleDebug; then + exit_on_error "Build failed for '$RELEASE_VERSION_NAME' release with '$APK_VERSION_TAG' tag." + fi + + echo "Validating APK file" + if ! test -f "$APK_DIR_PATH/${APK_BASENAME_PREFIX}.apk"; then + files_found="$(ls "$APK_DIR_PATH")" + exit_on_error "Failed to find built APK file at '$APK_DIR_PATH/${APK_BASENAME_PREFIX}.apk'. Files found: "$'\n'"$files_found" + fi + + echo "Generating checksums-sha256.txt file" + if ! (cd "$APK_DIR_PATH"; sha256sum "${APK_BASENAME_PREFIX}.apk" > checksums-sha256.txt); then + exit_on_error "Generate checksums-sha256.txt file failed for '$RELEASE_VERSION_NAME' release." + fi + echo "checksums-sha256.txt:"$'\n```\n'"$(cat "$APK_DIR_PATH/checksums-sha256.txt")"$'\n```' + + - name: Upload files to action + uses: actions/upload-artifact@v5 + with: + name: ${{ env.APK_BASENAME_PREFIX }} + path: | + ${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}.apk + ${{ env.APK_DIR_PATH }}/checksums-sha256.txt + ${{ env.APK_DIR_PATH }}/output-metadata.json diff --git a/.github/workflows/github_release_build.yml b/.github/workflows/github_release_build.yml new file mode 100644 index 0000000..f62c365 --- /dev/null +++ b/.github/workflows/github_release_build.yml @@ -0,0 +1,64 @@ +name: GitHub Release Build + +on: + release: + types: + - published + +jobs: + build: + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Clone repository + uses: actions/checkout@v4 + with: + ref: ${{ env.GITHUB_REF }} + + - name: Build and upload files to release + shell: bash {0} + 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 release version '${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+github.debug" + APK_BASENAME_PREFIX="termux-tasker-app_$APK_VERSION_TAG" + + echo "Building APK file for '$RELEASE_VERSION_NAME' release with '$APK_VERSION_TAG' tag" + export TERMUX_TASKER_APP__BUILD__APK_VERSION_TAG="$APK_VERSION_TAG" # Used by app/build.gradle + if ! ./gradlew assembleDebug; then + exit_on_error "Build failed for '$RELEASE_VERSION_NAME' release with '$APK_VERSION_TAG' tag." + fi + + echo "Validating APK file" + if ! test -f "$APK_DIR_PATH/${APK_BASENAME_PREFIX}.apk"; then + files_found="$(ls "$APK_DIR_PATH")" + exit_on_error "Failed to find built APK file at '$APK_DIR_PATH/${APK_BASENAME_PREFIX}.apk'. Files found: "$'\n'"$files_found" + fi + + echo "Generating checksums-sha256.txt file" + if ! (cd "$APK_DIR_PATH"; sha256sum "${APK_BASENAME_PREFIX}.apk" > checksums-sha256.txt); then + exit_on_error "Generate checksums-sha256.txt file failed for '$RELEASE_VERSION_NAME' release." + fi + echo "checksums-sha256.txt:"$'\n```\n'"$(cat "$APK_DIR_PATH/checksums-sha256.txt")"$'\n```' + + echo "Uploading files to release" + if ! gh release upload "$RELEASE_VERSION_NAME" \ + "$APK_DIR_PATH/${APK_BASENAME_PREFIX}.apk" \ + "$APK_DIR_PATH/checksums-sha256.txt" \ + ; then + exit_on_error "Upload files to release failed for '$RELEASE_VERSION_NAME' release." + fi diff --git a/.gitignore b/.gitignore index 774d5d8..9da7ecf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ build/ -local.properties +release/ .gradle/ .idea/ *.iml + +local.properties +github.properties diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..9eafb90 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1 @@ +The `termux/termux-tasker` repository is released under [GPLv3 only](https://www.gnu.org/licenses/gpl-3.0.html) license. diff --git a/README.md b/README.md index fe3a811..d945dad 100644 --- a/README.md +++ b/README.md @@ -3,51 +3,69 @@ [![Build status](https://github.com/termux/termux-tasker/workflows/Build/badge.svg)](https://github.com/termux/termux-tasker/actions) [![Join the chat at https://gitter.im/termux/termux](https://badges.gitter.im/termux/termux.svg)](https://gitter.im/termux/termux) -A [Termux] add-on app allowing `Termux` commands to be executed -from [Tasker] and other plugin apps. +A [Termux] plugin app allowing `Termux` commands to be executed from [Tasker] and other plugin host apps. ## ### Contents -- [Installation](#Installation) -- [Setup Instructions](#Setup-Instructions) -- [Usage](#Usage) -- [Plugin Configuration](#Plugin-Configuration) -- [Plugin Variables](#Plugin-Variables) -- [Templates](#Templates) -- [Creating And Modifying Scripts](#Creating-And-Modifying-Scripts) -- [Debugging](#Debugging) -- [Worthy Of Note](#Worthy-Of-Note) -- [License](#License) +- [Installation](#installation) +- [Setup Instructions](#setup-instructions) +- [Usage](#usage) +- [Plugin Configuration](#plugin-configuration) +- [Plugin Variables](#plugin-variables) +- [Templates](#templates) +- [Creating And Modifying Scripts](#creating-and-modifying-scripts) +- [Debugging](#debugging) +- [Worthy Of Note](#worthy-of-note) +- [For Maintainers and Contributors](#for-maintainers-and-contributors) +- [Forking](#forking) ## ### Installation -`Termux:Tasker` application can be obtained from [F-Droid](https://f-droid.org/en/packages/com.termux.tasker/). +Latest version is `v0.9.0`. -Additionally we provide per-commit debug builds for those who want to try -out the latest features or test their pull request. This build can be obtained -from one of the workflow runs listed on [Github Actions](https://github.com/termux/termux-tasker/actions) -page. +Check [`termux-app` Installation](https://github.com/termux/termux-app#Installation) for details before reading forward. +### F-Droid -The APK files in F-Droid releases or Github Actions builds 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 from different source. Android Package Manager will also normally not allow installation of APKs with a different signatures and you will get an error on installation but 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 app or its plugin APKs from your device first, then install all new APKs from the same new source again. The `~/.termux/tasker/` directory will not be accessible to the plugin and commands will not execute if `Termux` and `Termux:Tasker` APKs have a different signatures. +`Termux:Tasker` application can be obtained from `F-Droid` from [here](https://f-droid.org/en/packages/com.termux.tasker/). + +You **do not** need to download the `F-Droid` app (via the `Download F-Droid` link) to install `Termux:Tasker`. You can download the `Termux:Tasker` APK directly from the site by clicking the `Download APK` link at the bottom of each version section. + +It usually takes a few days (or even a week or more) for updates to be available on `F-Droid` once an update has been released on `Github`. The `F-Droid` releases are built and published by `F-Droid` once they [detect](https://gitlab.com/fdroid/fdroiddata/-/blob/master/metadata/com.termux.tasker.yml) a new `Github` release. The Termux maintainers **do not** have any control over the building and publishing of the Termux apps on `F-Droid`. Moreover, the Termux maintainers also do not have access to the APK signing keys of `F-Droid` releases, so we cannot release an APK ourselves on `Github` that would be compatible with `F-Droid` releases. + +The `F-Droid` app often may not notify you of updates and you will manually have to do a pull down swipe action in the `Updates` tab of the app for it to check updates. Make sure battery optimizations are disabled for the app, check https://dontkillmyapp.com/ for details on how to do that. + +### Github + +`Termux:Tasker` application can be obtained on `Github` either from [`Github Releases`](https://github.com/termux/termux-tasker/releases) for version `>= 0.6.0` or from [`Github Actions`](https://github.com/termux/termux-tasker/actions/workflows/github_action_build.yml?query=branch%3Amaster+event%3Apush). + +The APKs for `Github Releases` will be listed under `Assets` drop-down of the release. These are automatically attached when a new version is released. + +The APKs for `Github Actions` will be listed under `Artifacts` section of the 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. + +### Google Play Store **(Deprecated)** + +**Termux and its plugins are no longer updated on [Google Play Store](https://play.google.com/store/apps/details?id=com.termux.tasker) due to [android 10 issues](https://github.com/termux/termux-packages/wiki/Termux-and-Android-10) and have been deprecated. It is highly recommended to not install Termux apps from Play Store any more.** Check https://github.com/termux/termux-app#google-play-store-deprecated for details. ## ### Setup Instructions -#### Install Termux app (Mandatory) -The `Termux:Tasker` plugin requires [Termux] app to run the actual commands. You need to install it and start it at least once and have it install the required files for the plugin to start working. The Termux prefix directory `/data/data/com.termux/files/usr/` and Termux home directory `/data/data/com.termux/files/home/` must also exist and must have read, write and execute permissions `(0700)` for the plugin to work. The `$PREFIX/` is shortcut for the Termux prefix directory. The `~/` is a shortcut for the Termux home directory. Permissions and ownerships can be checked with the `stat ` command. +#### Install `Termux` app (Mandatory) +The `Termux:Tasker` plugin requires [Termux] app to run the actual commands. You need to install it and start it at least once and have it install the bootstrap files for the plugin to start working. The Termux prefix directory `/data/data/com.termux/files/usr/` and Termux home directory `/data/data/com.termux/files/home/` must also exist and must have read, write and execute permissions `(0700)` for the plugin to work. The `$PREFIX/` is shortcut for the Termux [prefix directory](https://github.com/termux/termux-packages/wiki/Termux-file-system-layout) and can also be referred by the `$PREFIX` shell environment variable. The `~/` is a shortcut for the Termux home directory and can also be referred by the `$HOME` shell environment variable. Note that `~/` will not expand inside single or double quotes when running commands. Permissions and ownerships can be checked with the `stat ` command. #### `com.termux.permission.RUN_COMMAND` permission (Mandatory) -For `Termux:Tasker` version `>= 0.5`, the plugin host app will need to be granted the `com.termux.permission.RUN_COMMAND` permission to run **ANY** plugin commands. This is a security measure to prevent any other apps from running commands in `Termux` context which do not have the required permission granted to them. This is also required for the [RUN_COMMAND Intent] intent. +For `Termux:Tasker` version `>= 0.5`, the plugin host app will need to be granted the `com.termux.permission.RUN_COMMAND` permission to run **ANY** plugin commands. This is a security measure to prevent any other apps from running commands in `Termux` context which do not have the required permission granted to them. This is also required for the [RUN_COMMAND Intent]. The [Tasker] app has requested the permission since `v5.9.3`, so you will need to update the app if you are using an older version. You can grant the permission using the `Permissions` activity in the `App Info` activity of your plugin host app. For `Tasker` you can grant it with: `Android Settings` -> `Apps` -> `Tasker` -> `Permissions` -> `Additional permissions` -> `Run commands in Termux environment`. @@ -72,15 +90,15 @@ chmod 700 -R /data/data/com.termux/files/home/.termux To set `allow-external-apps` property to `true`. -``` + ``` value="true"; key="allow-external-apps"; file="/data/data/com.termux/files/home/.termux/termux.properties"; mkdir -p "$(dirname "$file")"; chmod 700 "$(dirname "$file")"; if ! grep -E '^'"$key"'=.*' $file &>/dev/null; then [[ -s "$file" && ! -z "$(tail -c 1 "$file")" ]] && newline=$'\n' || newline=""; echo "$newline$key=$value" >> "$file"; else sed -i'' -E 's/^'"$key"'=.*/'"$key=$value"'/' $file; fi -``` + ``` To set `allow-external-apps` property to `false`. -``` + ``` value="false"; key="allow-external-apps"; file="/data/data/com.termux/files/home/.termux/termux.properties"; mkdir -p "$(dirname "$file")"; chmod 700 "$(dirname "$file")"; if ! grep -E '^'"$key"'=.*' $file &>/dev/null; then [[ -s "$file" && ! -z "$(tail -c 1 "$file")" ]] && newline=$'\n' || newline=""; echo "$newline$key=$value" >> "$file"; else sed -i'' -E 's/^'"$key"'=.*/'"$key=$value"'/' $file; fi -``` + ``` - Manual You can do it manually by running the below commands to open the `nano` text editor in the terminal. Then add/update a line `allow-external-apps=true` to set the property to `true`, and press `Ctrl+o` and then `Enter` to save and `Ctrl+x` to exit. @@ -113,32 +131,88 @@ For android `>= 10` there are new [restrictions](https://developer.android.com/g ### Plugin Configuration -The plugin configuration activity can be started by plugin host apps to configure the plugin to define what commands should be run and in which mode. Currently, there are 3 text fields, the `Executable`, `Arguments` and `Working directory path` fields and a `Execute in a terminal session` toggle. The text fields support plugin host app local variables (all lowercase) like `%executable`, `%arguments` or `%workdir`, you may use multiple variables in a single field. +The plugin configuration activity can be started by plugin host apps to configure the plugin to define what commands should be run and in which mode. + +The text fields support plugin host app local variables (all lowercase) like `%executable`, `%arguments`, `%workdir`, `%stdin`, etc and you may use multiple variables in a single field. + +#### `Executable` + +The `Executable` text field defines the executable that needs to be run. It can either be set to a file in `~/.termux/tasker/` directory or to an absolute path if `allow-external-apps` property is set to `true` (check [Setup Instructions](#Setup-Instructions)). Absolute paths can be like `/data/data/com.termux/files/usr/bin/bash`. The `$PREFIX/` and `~/` prefixes are also supported, like `$PREFIX/bin/bash` or `~/some-script`. + +Execute permissions will automatically be set for the executable file if it exists inside the `~/.termux/tasker/` directory when the plugin action is run. It is the user's responsibility to set read and execute permissions for the executable file if it exists outside the `~/.termux/tasker/` directory. That can be done by running the command `chmod 700 "/path/to/executable"` from a terminal session before running the plugin action. + + +#### `Arguments` + +The `Arguments` text field defines the argument that will be passed to the executable. For `Termux:Tasker` version `>= 0.5`, arguments will be processed just like there are if commands are run in a shell like bourne shell. It uses [ArgumentTokenizer](https://sourceforge.net/p/drjava/git_repo/ci/master/tree/drjava/src/edu/rice/cs/util/ArgumentTokenizer.java) to parse the arguments string. + +Arguemnts are split on a space ` `, tab `\t` or a newline `\n` ([`Character.isWhitespace()`](https://developer.android.com/reference/java/lang/Character#isWhitespace(char))) unless quoted with single `'` or double quotes `"`. Double quotes and backslashes can be escaped with backslashes in arguments surrounded with double quotes. Using backslash at end of line (`\\n`) to start a new argument is not supported and it will escape the newline and add it to start of the new argument. Internally, [`ArgumentTokenizer`](https://github.com/termux/termux-app/blob/master/termux-shared/src/main/java/com/termux/shared/shell/ArgumentTokenizer.java) class is using to convert the arguments string to separate arguments. + +Any argument surrounded with single quotes is considered a literal string. However, if an argument itself contains single quotes, then they will need to be escaped properly. You can escape them by replacing all single quotes `'` in an argument value with `'\''` **before** passing the argument surrounded with single quotes. So an argument surrounded with single quotes that would have been passed like `'some arg with single quote ' in it'` will be passed as `'some arg with single quote '\'' in it'`. This is basically 3 parts `'some arg with single quote '`, `\'` and `' in it'` but when processed, it will be considered as one single argument with the value `some arg with single quote ' in it` that is passed to the executable. + +For `Tasker`, you can use the `Variable Search Replace` action on an `%argument` variable to escape the single quotes. Set the `Search` field to one single quote `'`, and enable `Replace Matches` toggle, and set `Replace With` field to one single quote, followed by two backslashes, followed by two single quotes `'\\''`. The double backslash is to escape the backslash character itself. + + +#### `Working directory path` + +The `Working directory path` text field for `Termux:Tasker` version `>= 0.5` defines the working directory that should be used while running the command. The directory must be readable by the termux app. It is the user's responsibility to create the directory if its outside the `~/` directory for version `< 0.6.0` and `/data/data/com.termux/files` directory for version `>= 0.6.0`. That can be done by running the command `mkdir -p "/path/to/workdir"` from a terminal session before running the plugin action. The `$PREFIX/` and `~/` prefixes are also supported, like `$PREFIX/some-directory` or `~/some-directory`. + + +#### `Stdin` + +The `Stdin` text field for `Termux:Tasker` version `>= 0.6.0` can be used to pass scripts via standard input (`stdin`), like a `bash` script to the `$PREFIX/bin/bash` shell and a `python` script to the `$PREFIX/bin/python` shell or any other commands. This allows scripts to be defined in the plugin host app instead of defining physical script files in `~/.termux/tasker/` directory. Check [Defining Scripts In Plugin Host App](#defining-scripts-in-plugin-host-app) for details. -- The `Executable` field defines the executable that needs to be run. It can either be set to a file in `~/.termux/tasker/` directory or to an absolute path if `allow-external-apps` property is set to `true` (check [Setup Instructions](#Setup-Instructions)). Absolute paths can be like `/data/data/com.termux/files/usr/bin/bash`. The `$PREFIX/` and `~/` prefixes are also supported, like `$PREFIX/bin/bash` or `~/some-script`. +Note that if passing script via `stdin`, do not pass arguments, since it will fail depending on shell, at least will for `bash`. - Execute permissions will automatically be set for the executable file if it exists inside the `~/.termux/tasker/` directory when the plugin action is run. It is the user's responsibility to set read and execute permissions for the executable file if it exists outside the `~/.termux/tasker/` directory. That can be done by running the command `chmod 700 "/path/to/executable"` from a terminal session before running the plugin action. +The max supported length of a script is `45K` characters taking into consideration the Tasker plugin bundle limits of `100KB` when its stored in a [`Parcel`](https://developer.android.com/reference/android/os/Parcel). On Android `7-11`, the `String` characters are [stored in Parcel as `UTF-16`](https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/core/java/android/os/Parcel.java;l=773), i.e `2` bytes. So we use `10KB` for rest of the plugin configuration data and half of the remaining (`90KB`) for storing the script. -- The `Arguments` field defines the argument that will be passed to the executable. For `Termux:Tasker` version `>= 0.5`, arguments will be processed just like there are if commands are run in a shell like bourne shell. It uses [ArgumentTokenizer](https://sourceforge.net/p/drjava/git_repo/ci/master/tree/drjava/src/edu/rice/cs/util/ArgumentTokenizer.java) to parse the arguments string. +#### `Terminal Session Action` - Arguemnts are split on whitespaces unless quoted with single or double quotes. Double quotes and backslashes can be escaped with backslashes in arguments surrounded with double quotes. +The `Terminal Session Action` text field for `Termux:Tasker` version `>= 0.6.0` defines what should happen when a foreground session command is received for the `Termux`. The user can define whether the new session should be automatically switched to or if existing session should remain as the current session. The user can also define if foreground session commands should open the `TermuxActivity` or if they should run in the *"background"* in the Termux notification. The user can click the notification to open the sessions. The valid values are defined by [`TermuxConstants.TERMUX_APP.TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_*`](https://github.com/termux/termux-app/blob/v0.117/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java#L856), currently, between `0` and `3`. - Any argument surrounded with single quotes is considered a literal string. However, if an argument itself contains single quotes, then they will need to be escaped properly. You can escape them by replacing all single quotes `'` in an argument value with `'\''` **before** passing the argument surrounded with single quotes. So an argument surrounded with single quotes that would have been passed like `'some arg with single quote ' in it'` will be passed as `'some arg with single quote '\'' in it'`. This is basically 3 parts `'some arg with single quote '`, `\'` and `' in it'` but when processed, it will be considered as one single argument with the value `some arg with single quote ' in it` that is passed to the executable. - For `Tasker`, you can use the `Variable Search Replace` action on an `%argument` variable to escape the single quotes. Set the `Search` field to one single quote `'`, and enable `Replace Matches` toggle, and set `Replace With` field to one single quote, followed by two backslashes, followed by two single quotes `'\\''`. The double backslash is to escape the backslash character itself. +#### `Custom Log Level` +The `Custom Log Level` text field for `Termux:Tasker` version `>= 0.6.0` defines the log level for background commands that should be used by `Termux`. -- The `Working directory path` field for `Termux:Tasker` version `>= 0.5` defines the working directory that should be used while running the command. The directory must be readable by the termux app. It is the user's responsibility to create the directory if its outside the `~/` directory. That can be done by running the command `mkdir -p "/path/to/workdir"` from a terminal session before running the plugin action. The `$PREFIX/` and `~/` prefixes are also supported, like `$PREFIX/some-directory` or `~/some-directory`. +By default Termux only logs command `stdout` and `stderr` to `logcat` if user has set log level to `VERBOSE` in `Termux` app settings (not `Termux:Tasker`). However, if command outputted too much data to `logcat`, then `logcat` clients like in Android Studio would crash. +So one can pass a custom log level that is `>=` to the log level set it `Termux` app settings where (`OFF=0`, `NORMAL=1`, `DEBUG=2`, `VERBOSE=3`) for custom behaviour. If you pass `0`, it will completely disable logging. If you pass `1`, logging will only be enabled if log level in termux settings is `NORMAL` or higher. If custom log level is not passed, then default behaviour will remain and log level in `Termux` app settings must be `VERBOSE` or higher for logging to be enabled. Note that the log entries will still be logged with priority `Log.VERBOSE` regardless of log level, i.e `logcat` will have `V/`. -- `Execute in a terminal session` toggle defines whether the commands will be run in the background or in a foreground terminal session. +The entries `logcat` component will be `TermuxCommand`. For output at `stdout`, the entry format is `[-stdout] ...` and for the output at `stderr`, the entry format is `[-stderr] ...`. The `` will be the process id (`pid`) as an integer that was started by Termux for the executable. For example: `V/TermuxCommand: [66666-stdout] ...`. - If the toggle is **enabled**, a new terminal session will open up automatically in the foreground and commands will be run inside it. Result of commands is **not returned** to the plugin host app. - If the toggle is **not enabled**, then commands are run in the background and result of commands **is returned** to the plugin host app in `%stdout`, `%stderr` and `%result` variables. +##### Using `Custom Log Level` to send commands to Tasker + +
+ + +Instead of using `am` command to send messages back to Tasker, you can use Tasker `Logcat Entry` profile event to listen to messages from Termux at both `stdout` and `stderr`. This might be faster than `am` command intent systems or at least possibly more convenient in some use cases. + +So setup a profile with the `Component` value set to `TermuxCommand` and `Filter` value set to `-E 'TermuxCommand: \[[0-9]+-((stdout)|(stderr))\] message_tag: .*'` and enable the `Grep Filter` toggle so that entry matching is done in native code. Check Tasker [Logcat Info](https://github.com/joaomgcd/TaskerDocumentation/blob/master/en/help/logcat%20info.md) documentation for details. Also enable `Enforce Task Order` in profile settings and set collision handling to `Run Both Together` so that if two or more entries are sent quickly, entry task is run for all. Tasker currently (`v5.13.16`) is not maintaining order of entry tasks despite the setting. + +Then start a `Termux:Tasker` plugin action with custom log level `1` (assuming current log level is `NORMAL`) and you should be able to receive the entries for whatever you send to `stdout` and `stderr` in your script that starts with `message_tag: ` as `%lc_text` in entry task of tasker `Logcat Entry` profile. You can remove the prefix from the `%lc_text` variable with `Variable Search Replace` action. Set the `Search` field to `TermuxCommand: \[[0-9]+-((stdout)|(stderr))\] message_tag: `, and enable `Replace Matches` toggle, and leave `Replace With` empty. + +
+ + + +#### `Execute in a terminal session` + +The `Execute in a terminal session` toggle defines whether the commands will be run in the background or in a foreground terminal session. + +If the toggle is **enabled**, a new terminal session will open up automatically in the foreground and commands will be run inside it. Result of commands is **not returned** to the plugin host app for version `< 0.6.0`. For version `>= 0.6.0`, result will be returned in `%stdout` and `%result` variables. The `%stdout` variable will only contain the session transcript and will contain both `stdout` and `stderr` combined, basically anything sent to the the pseudo terminal `/dev/pts`, including `PS1` prefixes for interactive sessions. For foreground commands that exited with failure will require `Termux` app version `>= 0.118.0` for sessions to automatically close without waiting for user to press enter. + +If the toggle is **not enabled**, then commands are run in the background and result of commands **is returned** to the plugin host app in `%stdout`, `%stderr` and `%result` variables. + +Check [Setup Instructions](#Setup-Instructions) for android `>= 10` restrictions that will prevent the commands from automatically starting unless notification is clicked. + + +#### `Wait for result for commands` + +The `Wait for result for commands` toggle for `Termux:Tasker` version `>= 0.6.0` defines whether the plugin action should wait for result of commands. It will apply to both foreground session and background commands. Check [Plugin Variables](#plugin-variables) for details. - Check [Setup Instructions](#Setup-Instructions) for android `>= 10` restrictions that will prevent the commands from automatically starting unless notification is clicked. Check [Templates](#Templates) section for templates that can be used for various configurations. ## @@ -150,7 +224,9 @@ Check [Templates](#Templates) section for templates that can be used for various Depending on plugin configuration, the following variables may be returned. - `%stdout` containing `stdout` of commands. +- `%stdout_original_length` containing original length of `stdout`. - `%stderr` containing `stderr` of commands. +- `%stderr_original_length` containing original length of `stderr`. - `%result` containing `exit code` of commands. The `exit code` `0` often means success and anything else is usually a failure of some sort. - `%err` containing `exit code` of plugin action. This will be set only if running the action itself failed like missing permissions or invalid configuration. This may be set by the plugin host app or the plugin. This will not be set if plugin action succeeded. - `%errmsg` containing the error message of why the plugin action failed if `%err` is set. @@ -158,18 +234,23 @@ Depending on plugin configuration, the following variables may be returned. If the timeout value of the plugin action is set to `0` or `None` (slider to extreme left in Tasker), then **no variables will be returned**, regardless of whether commands need to be run in a foreground terminal session or in background. Even `%errmsg` will not be set to notify of any errors while running the plugin action since plugin host app will not wait for the plugin to return any variables. This is important for cases like if `allow-external-apps` is not set to `true` but an absolute path outside `~/.termux/tasker/` directory is set as the `Executable`, in which case the plugin action will appear to have succeeded but no commands will execute. -If the timeout value of the plugin action is set to `>0` and `Execute in a terminal session` is not enabled to run commands in background, then the result of commands will be returned in `%stdout`, `%stderr` and `%result` variables. The `%err` and `%errmsg`variables may also be set if the action failed. Note that if the timeout has passed by the time commands finish, the result of command variables will not be set in the plugin host app task and the action will exit with a timeout error, the `%err` variable will be set to `2` and `%errmsg` to `timeout`, at least in `Tasker`. +If the timeout value of the plugin action is set to `>0` and `Wait for result for commands` toggle is enabled, then the result of commands will be returned in `%stdout`, `%stderr` (only background) and `%result` variables. The `%err` and `%errmsg` variables may also be set if the action failed. Note that if the timeout has passed by the time commands finish, the result of command variables will not be set in the plugin host app task and the action will exit with a timeout error, the `%err` variable will be set to `2` and `%errmsg` to `timeout`, at least in `Tasker`. -If the timeout value of the plugin action is set to `>0` and `Execute in a terminal session` is enabled, then `%stdout`, `%stderr` and `%result` variables will **not** be returned. Only the `%err` and `%errmsg`variables may be set if the action failed. +If the timeout value of the plugin action is set to `>0` and `Wait for result for commands` toggle is not enabled, then `%stdout`, `%stderr` and `%result` variables will **not** be returned. Only the `%err` and `%errmsg`variables may be set if the action failed. The (new) default timeout is set to `10s` for all configurations. If you are running background commands that will likely take longer to run, then increase the timeout or set it to `Never` (slider to extreme right in Tasker). The plugin host app may still get killed by android if it keeps running for long time regardless of timeout value, check [here](https://tasker.joaoapps.com/userguide/en/faqs/faq-problem.html#00) for more info. Even if the commands are to be run in a foreground terminal session, **do not** set timeout to `0` but use `10s` instead, since with timeout `0`, plugin host app will not wait for any errors to be returned by the plugin in `%err` and `%errmsg` variables and continue the task and the user wouldn't know if any error occurred. Users who already have preexisting actions with the timeout set to `0`, like for foreground terminal session commands (considering previous default was `0`) should update their tasks and use the new default `10s` instead, just opening the configuration screen and returning should automatically do it.   + For `Termux:Tasker` version `>= 0.5`, the `%errmsg`, `%stdout`, `%stderr` and `%result` variables will also be automatically cleared whenever the action is run if timeout is greater than `0` to solve the issue of if multiple actions are run in the same task, then variables from previous action may still be set and get mixed in with current ones. For older versions, you can use a `Variable Clear` action in Tasker with `Pattern Matching` enabled and the `Name` field set to `%errmsg/%stdout/%stderr/%result` to clear all of them before each plugin action if multiple actions are run in a task or `Local Variable Passthrough` is enabled in Tasker. The `%err` and `%errmsg` variables will mainly only be set for `Termux:Tasker` version `>= 0.5`. These will be set if there are errors like if an executable file is not found, or if `allow-external-apps` property is not set to `true` but an absolute path is specified as the executable. `Tasker` itself may set it too like if `Tasker` has not been granted the `com.termux.permission.RUN_COMMAND` permission when running the plugin action or if a timeout occurs, etc. The `%err` and `%errmsg` variables must be stored in another variable with the `Variable Set` action right after the plugin action if they have to be checked and used later. The plugin host app like Tasker sets and clears `%err` for each action and it is only available in the next action. To check if and what they are set to, add a `Variable Clear` action for `%command_failed` variable before the plugin action, then add a `Variable Set` action after the plugin action for the `%command_failed` variable with the value `%err %errmsg` and `If` conditions `If %err Set OR If %errmsg Set`. Then you can just check `If %command_failed Set` afterward and flash it to notify the user or exit the task if necessary. Error checking should ideally also be done based on `%result` and optionally the `%stderr` variables before continuing the task. +  + + +The `%stdout_original_length` and `%stderr_original_length` can be used to check if `%stdout` and `%stderr` were truncated by `Termux` app before sending them back to plugin host app in case they were too large and would have triggered `TransactionTooLargeException`. The `stdout` and `stderr` sent back will be truncated from the start to max `100KB` combined. The `errmsg` will also be truncated from end to max `25KB`. Check [here](https://github.com/termux/termux-app/commit/f62febbf) and [here](https://github.com/termux/termux-app/commit/a2209ddd) for details. Check [Templates](#Templates) section for templates on how error and result variables should be handled for various configurations. ## @@ -178,13 +259,15 @@ Check [Templates](#Templates) section for templates on how error and result vari ### Templates +The templates were written for version `< 0.6.0` and currently have not been updated for version `>= 0.6.0`. + #### Tasker - `Tasks` - `XML` Download the [Termux Tasker Plugin Basic Templates Task XML](templates/plugin_hosts/tasker/Termux_Tasker_Plugin_Basic_Templates.tsk.xml) file to the android download directory. To download, right-click or hold the `Raw` button at the top after opening a file link and select `Download/Save link` or use `curl` from a termux shell. Then import the downloaded task file into Tasker by long pressing the `Task` tab button in Tasker home and selecting `Import Task`. - `curl -L 'https://github.com/termux/termux-tasker/raw/master/templates/plugin_hosts/tasker/Termux_Tasker_Plugin_Basic_Templates.tsk.xml' -o "/storage/emulated/0/Download/Termux_Tasker_Plugin_Basic_Templates.tsk.xml"` + `curl -L 'https://github.com/termux/termux-tasker/raw/master/templates/plugin_hosts/tasker/Termux_Tasker_Plugin_Basic_Templates.tsk.xml' -o "/sdcard/Download/Termux_Tasker_Plugin_Basic_Templates.tsk.xml"` - `Taskernet` Import `Termux Tasker Plugin Basic Templates Task` from `Taskernet` from [here](https://taskernet.com/shares/?user=AS35m8mXdvaT1Vj8TwkSaCaoMUv220IIGtHe3pG4MymrCUhpgzrat6njEOnDVVulhAIHLi6BPUt1&id=Task%3ATermux+Tasker+Plugin+Basic+Templates). @@ -204,9 +287,9 @@ Check [Templates](#Templates) section for templates on how error and result vari - Download them manually to android download directory and then use `cat` to copy them to `~/.termux/tasker/` or manually do it with a [SAF file browser](#Creating-And-Modifying-Scripts). - `cat "/storage/emulated/0/Download/termux_tasker_basic_bash_test" > "/data/data/com.termux/files/home/.termux/tasker/termux_tasker_basic_bash_test"` + `cat "/sdcard/Download/termux_tasker_basic_bash_test" > "/data/data/com.termux/files/home/.termux/tasker/termux_tasker_basic_bash_test"` - `cat "/storage/emulated/0/Download/termux_tasker_basic_python_test" > "/data/data/com.termux/files/home/.termux/tasker/termux_tasker_basic_python_test"` + `cat "/sdcard/Download/termux_tasker_basic_python_test" > "/data/data/com.termux/files/home/.termux/tasker/termux_tasker_basic_python_test"` 2. Set executable permissions. @@ -221,7 +304,7 @@ Check [Templates](#Templates) section for templates on how error and result vari `nano "/data/data/com.termux/files/home/.termux/tasker/termux_tasker_basic_python_test"` -Termux needs to be granted `Storage` permission to allow it to access `/storage/emulated/0/Download` directory, otherwise you will get permission denied errors while running commands. +Termux needs to be granted `Storage` permission to allow it to access `/sdcard/Download` directory, otherwise you will get permission denied errors while running commands. ## @@ -232,7 +315,7 @@ You can create scripts in `~/.termux/tasker/` directory after following its [Set You can use `shell` based text editors like `nano`, `vim` or `emacs` to create and modify scripts. -`nano "/data/data/com.termux/files/home/.termux/tasker/some_script"` +`nano ~/.termux/tasker/some_script` You can also use `GUI` based text editor android apps that support `SAF`. Termux provides a [Storage Access Framework (SAF)](https://wiki.termux.com/wiki/Internal_and_external_storage) file provider to allow other apps to access its `~/` home directory. However, the `$PREFIX/` directory is not accessible to other apps. The [QuickEdit] or [QuickEdit Pro] app does support `SAF` and can handle large files without crashing, however, it is closed source and its pro version without ads is paid. You can also use [Acode editor] or [Turbo Editor] if you want an open source app. @@ -243,15 +326,18 @@ Note that the android default `SAF` `Document` file picker may not support hidde ### Debugging -You can help debug problems like how arguments are being parsed by the plugin or if the plugin is even firing etc by setting appropriate `logcat` `Log Level` in options menu (3 dots) in the plugin configuration screen. Note that whatever log level is set will affect the entire plugin app and all plugin actions and not just for the action whose configuration you used to set it. The setting only exists inside the configuration activity of actions because creating a separate launcher activity that would be shown in the list of apps in the launcher just for this setting doesn't seem worth it. `Log Level` defaults to`Normal` and log level `Debug` 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. +You can help debug problems like how arguments are being parsed by the plugin or if the plugin is even firing etc by setting appropriate `logcat` `Log Level` in options menu (3 dots) in the plugin configuration screen or in `Termux` app settings -> `Termux:Tasker` -> `Debugging` -> `Log Level` (Requires `Termux` app version `>= 0.113`). Note that whatever log level is set will affect the entire plugin app and all plugin actions and not just for the action whose configuration you used to set it. The setting only exists inside the configuration activity of actions because creating a separate launcher activity that would be shown in the list of apps in the launcher just for this setting doesn't seem worth it. 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 **does not execute the commands itself** but sends an execution intent 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 `Termux:Tasker` app settings to get all the info. -For information on how to view the `logcat` logs, check official android guide [here](https://developer.android.com/studio/command-line/logcat). +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). ##### 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 + +- `Off` - Log nothing. +- `Normal` - Start logging error, warn and info messages and stacktraces. +- `Debug` - Start logging debug messages. +- `Verbose` - Start logging verbose messages. ## @@ -262,31 +348,43 @@ For information on how to view the `logcat` logs, check official android guide [ There are limits on the arguments size you can pass to commands or the full command string length that can be run, which is likely equal to `131072` bytes or `128KB` for an android device defined by `ARG_MAX` but after subtracting shell environment size, etc, it will roughly be around `120-125KB` but limits may vary for different android versions and kernels. You can check the limits for a given termux session by running `true | xargs --show-limits`. If you exceed the limit, you will get exceptions like `Argument list too long`. You can manually cross the limit by running something like `$PREFIX/bin/echo "$(head -c 131072 < /dev/zero | tr '\0' 'a')" | tr -d 'a'`, use full path of `echo`, otherwise the `echo` shell built-in will be called to which the limit does not apply since `exec` is not done. -Moreover, exchanging data between `Tasker` and `Termux:Tasker` is done using [Intents](https://developer.android.com/guide/components/activities/parcelables-and-bundles), like sending the command and receiving result of commands in `%stdout` and `%stderr`. However, android has limits on the size of *actual* data that can be sent through intents, it is roughly `500KB` on android `7` but may be different for different android versions. +Moreover, exchanging data between `Tasker` and `Termux:Tasker` is done using [Intents](https://developer.android.com/guide/components/activities/parcelables-and-bundles), like sending the command and receiving result of commands in `%stdout` and `%stderr`. However, android has limits on the size of *actual* data that can be sent through intents, it is roughly `500KB` on android `7` but may be different for different android versions. The `Termux` app based on testing still sets a safe limit at `100KB` and will truncate any data higher than that. Check `%stdout_original_length` and `%stderr_original_length` in [Plugin Variables](#plugin-variables) section for details. -Basically, make sure any data/arguments you send to `Termux:Tasker` is less than `120KB` (or whatever you found) and any expected result sent back is less than `500KB`, but best keep it as low as possible for greater portability. If you want to exchange an even larger data between tasker and termux, use physical files instead. +Basically, make sure any data/arguments you send to `Termux:Tasker` is less than `120KB` (or whatever you found) and any expected result sent back is less than `100KB`, but best keep it as low as possible for greater portability. If you want to exchange an even larger data between tasker and termux, use physical files instead. -The argument data limits also apply for the [RUN_COMMAND Intent] intent. +The argument data limits also apply for the [RUN_COMMAND Intent]. ##### Termux Environment -Termux does not load the environment fully for external plugins or [RUN_COMMAND Intent] commands, like setting `LD_PRELOAD`, so any *external* scripts which do not have shebangs to full path to termux bin directory will not work if called from inside your *plugin* scripts, since `libtermux-exec.so` is not called since `LD_PRELOAD` isn't set and you will get `bad interpreter: No such file or directory` errors. Simply setting `LD_PRELOAD` will not work either without starting a new shell. So make sure to set the shebangs correctly for any *external* scripts you want to run from inside your *plugin* script. The correct shebangs for termux scripts are like `#!/data/data/com.termux/files/usr/bin/bash` for bash scripts instead of `#!/usr/bin/bash` used in common linux distros. You can also use [termux-fix-shebang](https://wiki.termux.com/wiki/Termux-fix-shebang) command on the *external* scripts before running them with the plugin to fix the shebangs automatically. +Termux does not load the environment fully for external plugins or [RUN_COMMAND Intent] commands, like setting `LD_PRELOAD`, so any *external* scripts which do not have shebangs to full path to termux bin directory will not work if called from inside your *plugin* scripts, since `libtermux-exec.so` is not called since `LD_PRELOAD` isn't set and you will get `bad interpreter: No such file or directory` errors. Simply setting `LD_PRELOAD` will not work either without starting a new shell. So make sure to set the shebangs correctly for any *external* scripts you want to run from inside your *plugin* script. The correct shebangs for termux scripts are like `#!/data/data/com.termux/files/usr/bin/bash` for bash scripts instead of `#!/usr/bin/bash` used in common linux distros. You can also use [termux-fix-shebang](https://wiki.termux.com/wiki/Termux-fix-shebang) command on the *external* scripts before running them with the plugin to fix the shebangs automatically or use `tudo`/`sudo` mentioned below. ##### Defining Scripts In Plugin Host App -Currently, any script files that need to be run need to be created in `~/.termux/tasker/` directory. It may get inconvenient to create physical script files for each type of command you want to run. These script files are also neither part of backups of plugin host apps like Tasker and require separate backup methods and nor are part of project configs shared with other people or even between your own devices, and so the scripts need to be added manually to the `~/.termux/tasker/` directory on each device. To solve such issues and to dynamically define scripts of different interpreted languages inside your plugin host app like `Tasker` and to pass them to `Termux` as arguments instead of creating script files, [tudo](https://github.com/agnostic-apollo/tudo) can be used for running commands in termux user context and [sudo](https://github.com/agnostic-apollo/sudo) for running commands with super user (root) context, check their `script` command type. These scripts will also load the termux environment properly like setting `LD_PRELOAD` etc before running the commands. +Any script files that need to be run need to be created in `~/.termux/tasker/` directory. It may get inconvenient to create physical script files for each type of command you want to run. These script files are also neither part of backups of plugin host apps like Tasker and require separate backup methods and nor are part of project configs shared with other people or even between your own devices, and so the scripts need to be added manually to the `~/.termux/tasker/` directory on each device. + +To solve such issues and to dynamically define scripts of different interpreted languages inside your plugin host app like `Tasker` and to pass them to `Termux` as arguments instead of creating script files, you can either use [`Stdin`](#stdin) plugin configuration field or use `tudo` or `sudo`. + +The [`tudo`](https://github.com/agnostic-apollo/tudo) script can be used for running commands in termux user context and the [`sudo`](https://github.com/agnostic-apollo/sudo) script for running commands with super user (root) context, check their `script` command type. These scripts will also load the termux environment properly like setting `LD_PRELOAD` etc before running the commands. There are much more customizable then using `Stdin` and support things that aren't possible to be provided via the plugin. ## -### License +## For Maintainers and Contributors -Released under [the GPLv3 license](https://www.gnu.org/licenses/gpl.html). +Check [For Maintainers and Contributors](https://github.com/termux/termux-app#For-Maintainers-and-Contributors) section of `termux/termux-app` `README` for details. ## + +## Forking + +Check [Forking](https://github.com/termux/termux-app#Forking) section of `termux/termux-app` `README` for details. +## + + + [Termux]: https://termux.com [Tasker]: https://tasker.joaoapps.com [QuickEdit]: https://play.google.com/store/apps/details?id=com.rhmsoft.edit diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..479f15c --- /dev/null +++ b/SECURITY.md @@ -0,0 +1 @@ +Check https://termux.dev/security for info on Termux security policies and how to report vulnerabilities. diff --git a/app/build.gradle b/app/build.gradle index 3b4470d..942f764 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,19 +1,30 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 28 + namespace "com.termux.tasker" + + compileSdk project.properties.compileSdkVersion.toInteger() + def appVersionName = System.getenv("TERMUX_TASKER_APP__BUILD__APP_VERSION_NAME") ?: "" + def apkVersionTag = System.getenv("TERMUX_TASKER_APP__BUILD__APK_VERSION_TAG") ?: "" + defaultConfig { applicationId "com.termux.tasker" - minSdkVersion 24 - targetSdkVersion 28 - versionCode 5 - versionName "0.5" - testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + minSdk project.properties.minSdkVersion.toInteger() + targetSdk project.properties.targetSdkVersion.toInteger() + versionCode 1002 + versionName "0.9.0" + + if (appVersionName) versionName = appVersionName + validateVersionName(versionName) + + manifestPlaceholders.TERMUX_PACKAGE_NAME = "com.termux" + manifestPlaceholders.TERMUX_APP_NAME = "Termux" + manifestPlaceholders.TERMUX_TASKER_APP_NAME = "Termux:Tasker" } signingConfigs { debug { - storeFile file('dev_keystore.jks') + storeFile file('testkey_untrusted.jks') keyAlias 'alias' storePassword 'xrj45yWGLbsO7W0v' keyPassword 'xrj45yWGLbsO7W0v' @@ -23,7 +34,7 @@ android { buildTypes { release { minifyEnabled true - shrinkResources true + shrinkResources false // Reproducible builds proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } @@ -34,22 +45,67 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + // Flag to enable support for the new language APIs + coreLibraryDesugaringEnabled true + + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + + applicationVariants.all { variant -> + variant.outputs.all { output -> + outputFileName = new File("termux-tasker-app_" + + (apkVersionTag ? apkVersionTag : "v" + versionName + "+" + variant.buildType.name) + ".apk") + } + } + + packagingOptions { + // Remove terminal-emulator and termux-shared JNI libs added via termux-shared dependency + exclude "lib/*/libtermux.so" + exclude "lib/*/liblocal-socket.so" } } dependencies { - implementation 'com.google.android.material:material:1.2.1' + coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.1.5" - testImplementation 'junit:junit:4.13.1' - androidTestImplementation 'androidx.test.ext:junit:1.1.2' - androidTestImplementation 'androidx.test:runner:1.3.0' - androidTestImplementation 'androidx.test:rules:1.3.0' + testImplementation "junit:junit:4.13.2" + androidTestImplementation "androidx.test.ext:junit:1.2.1" + androidTestImplementation "androidx.test:runner:1.6.2" + androidTestImplementation "androidx.test:rules:1.6.1" + + implementation "androidx.appcompat:appcompat:1.7.0" + implementation "androidx.annotation:annotation:1.9.1" + implementation "com.google.android.material:material:1.12.0" + implementation "com.google.guava:guava:24.1-jre" + + implementation "com.termux.termux-app:termux-shared:8aca6dbbf4" + // Use if below libraries are published locally by termux-app with `./gradlew publishReleasePublicationToMavenLocal` and used with `mavenLocal()`. + // If updates are done, republish there and sync project with gradle files here + // https://github.com/termux/termux-app/wiki/Termux-Libraries + //implementation "com.termux:termux-shared:0.118.0" + + implementation "com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava" + + constraints { + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0") { + because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib") + } + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0") { + because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib") + } + } } task versionName { - doLast { - print android.defaultConfig.versionName - } + doLast { + print android.defaultConfig.versionName + } +} + +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.") } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index c40c6b8..bd0e223 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,17 +1,10 @@ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified -# in /Users/fornwall/lib/android-sdk/tools/proguard/proguard-android.txt +# in android-sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html -# Add any project specific keep options here: - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} +-dontobfuscate diff --git a/app/src/androidTest/java/com/termux/tasker/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/termux/tasker/ExampleInstrumentedTest.java index e1ed867..00e88e4 100644 --- a/app/src/androidTest/java/com/termux/tasker/ExampleInstrumentedTest.java +++ b/app/src/androidTest/java/com/termux/tasker/ExampleInstrumentedTest.java @@ -4,6 +4,8 @@ import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner; +import com.termux.shared.termux.TermuxConstants; + import org.junit.Test; import org.junit.runner.RunWith; @@ -22,6 +24,6 @@ public void useAppContext() { // Context of the app under test. Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); - assertEquals("com.termux.tasker", appContext.getPackageName()); + assertEquals(TermuxConstants.TERMUX_TASKER_PACKAGE_NAME, appContext.getPackageName()); } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e65b8a6..c5c7255 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,14 +1,32 @@ + xmlns:tools="http://schemas.android.com/tools" + android:sharedUserId="${TERMUX_PACKAGE_NAME}" + android:sharedUserLabel="@string/shared_user_label"> + android:allowBackup="true" + android:fullBackupOnly="false" + android:supportsRtl="true"> + + + + + + + + + + @@ -30,6 +50,14 @@ + + + - - + + - + \ No newline at end of file diff --git a/app/src/main/java/com/termux/tasker/AbstractPluginActivity.java b/app/src/main/java/com/termux/tasker/AbstractPluginActivity.java index 15b9be1..e57fb95 100644 --- a/app/src/main/java/com/termux/tasker/AbstractPluginActivity.java +++ b/app/src/main/java/com/termux/tasker/AbstractPluginActivity.java @@ -4,8 +4,6 @@ import android.view.Menu; import android.view.MenuItem; -import com.termux.tasker.utils.Logger; - /** * Superclass for plug-in Activities. This class takes care of initializing aspects of the plug-in's UI to * look more integrated with the plug-in host. @@ -51,7 +49,8 @@ public boolean onOptionsItemSelected(final MenuItem item) { * During {@link #finish()}, subclasses can call this method to determine whether the Activity was * canceled. * - * @return True if the Activity was canceled. False if the Activity was not canceled. + * @return Returns {@code true} if the Activity was canceled, otherwise returns {@code false} + * if the Activity was not canceled. */ protected boolean isCanceled() { return mIsCancelled; diff --git a/app/src/main/java/com/termux/tasker/BundleScrubber.java b/app/src/main/java/com/termux/tasker/BundleScrubber.java index fd87662..16e0fd6 100644 --- a/app/src/main/java/com/termux/tasker/BundleScrubber.java +++ b/app/src/main/java/com/termux/tasker/BundleScrubber.java @@ -14,9 +14,10 @@ public final class BundleScrubber { * Bundle is null, has no extras, or the extras do not contain a private serializable subclass, the Bundle * is not mutated. * - * @param intent {@code Intent} to scrub. This parameter may be mutated if scrubbing is necessary. This - * parameter may be null. - * @return true if the Intent was scrubbed, false if the Intent was not modified. + * @param intent The {@link Intent} to scrub. This parameter may be mutated if scrubbing is + * necessary. This parameter may be {@code null}. + * @return Returns {@code true} if the Intent was scrubbed, otherwise returns {@code false} if + * the {@link Intent} was not modified. */ public static boolean scrub(final Intent intent) { return null != intent && scrub(intent.getExtras()); @@ -27,9 +28,10 @@ public static boolean scrub(final Intent intent) { * private serializable subclass, the Bundle is cleared. If the Bundle is null, has no extras, or the * extras do not contain a private serializable subclass, the Bundle is not mutated. * - * @param bundle {@code Bundle} to scrub. This parameter may be mutated if scrubbing is necessary. This - * parameter may be null. - * @return true if the Bundle was scrubbed, false if the Bundle was not modified. + * @param bundle The {@link Bundle} to scrub. This parameter may be mutated if scrubbing is necessary. + * This parameter may be {@code null}. + * @return Returns {@code true} if the Bundle was scrubbed, otheriwse {@code false} if the + * {@link Bundle} was not modified. */ public static boolean scrub(final Bundle bundle) { if (null == bundle) return false; diff --git a/app/src/main/java/com/termux/tasker/Constants.java b/app/src/main/java/com/termux/tasker/Constants.java deleted file mode 100644 index b647518..0000000 --- a/app/src/main/java/com/termux/tasker/Constants.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.termux.tasker; - -import java.io.File; - -public final class Constants { - - public static final String TERMUX_PACKAGE = "com.termux"; - public static final String TERMUX_SERVICE = "com.termux.app.TermuxService"; - - public static final String FILES_PATH = "/data/data/com.termux/files"; - public static final String PREFIX_PATH = FILES_PATH + "/usr"; - public static final String HOME_PATH = FILES_PATH + "/home"; - public static final String TASKER_PATH = HOME_PATH + "/.termux/tasker"; - - public static final File FILES_DIR = new File(FILES_PATH); - public static final File PREFIX_DIR = new File(PREFIX_PATH); - public static final File HOME_DIR = new File(HOME_PATH); - public static final File TASKER_DIR = new File(TASKER_PATH); - - public static final String PERMISSION_RUN_COMMAND = "com.termux.permission.RUN_COMMAND"; - - public static final String ALLOW_EXTERNAL_APPS_PROPERTY = "allow-external-apps"; - public static final String ALLOW_EXTERNAL_APPS_PROPERTY_DEFAULT_VALUE = "false"; - -} diff --git a/app/src/main/java/com/termux/tasker/EditConfigurationActivity.java b/app/src/main/java/com/termux/tasker/EditConfigurationActivity.java index a68f78c..e0fbdd8 100644 --- a/app/src/main/java/com/termux/tasker/EditConfigurationActivity.java +++ b/app/src/main/java/com/termux/tasker/EditConfigurationActivity.java @@ -1,14 +1,31 @@ package com.termux.tasker; +import android.app.Activity; import android.content.Intent; +import android.graphics.Typeface; import android.os.Bundle; import com.google.android.material.textfield.TextInputEditText; -import com.termux.tasker.utils.FileUtils; -import com.termux.tasker.utils.Logger; +import com.google.android.material.textfield.TextInputLayout; +import com.termux.shared.activities.TextIOActivity; +import com.termux.shared.activity.media.AppCompatActivityUtils; +import com.termux.shared.data.DataUtils; +import com.termux.shared.data.IntentUtils; +import com.termux.shared.errors.Error; +import com.termux.shared.logger.Logger; +import com.termux.shared.models.TextIOInfo; +import com.termux.shared.termux.TermuxConstants; +import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE; +import com.termux.shared.file.FileUtils; +import com.termux.shared.termux.TermuxUtils; +import com.termux.shared.termux.file.TermuxFileUtils; +import com.termux.shared.termux.theme.TermuxThemeUtils; +import com.termux.shared.theme.NightMode; +import com.termux.tasker.utils.LoggerUtils; import com.termux.tasker.utils.PluginUtils; -import com.termux.tasker.utils.TermuxAppUtils; +import com.termux.tasker.utils.TaskerPlugin; -import androidx.appcompat.app.ActionBar; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import android.text.Editable; import android.text.TextWatcher; @@ -18,11 +35,16 @@ import android.widget.ArrayAdapter; import android.widget.AutoCompleteTextView; import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.EditText; import android.widget.TextView; import java.io.File; import java.util.ArrayList; import java.util.Arrays; +import java.util.List; + +import static com.termux.tasker.utils.TaskerPlugin.Setting.RESULT_CODE_FAILED; /** * This is the "Edit" activity for a Locale Plug-in. @@ -36,43 +58,75 @@ */ public final class EditConfigurationActivity extends AbstractPluginActivity { + private TextInputLayout mExecutablePathTextLayout; private AutoCompleteTextView mExecutablePathText; private TextInputEditText mArgumentsText; + private TextInputLayout mWorkingDirectoryPathTextLayout; private AutoCompleteTextView mWorkingDirectoryPathText; + private TextView mStdinView; + private TextInputLayout mSessionActionLayout; + private TextInputEditText mSessionAction; + private TextInputLayout mBackgroundCustomLogLevelLayout; + private TextInputEditText mBackgroundCustomLogLevel; private CheckBox mInTerminalCheckbox; + private CheckBox mWaitForResult; private TextView mExecutableAbsolutePathText; private TextView mWorkingDirectoryAbsolutePathText; private TextView mTermuxAppFilesPathInaccessibleWarning; private TextView mPluginPermissionUngrantedWarning; private TextView mAllowExternalAppsUngrantedWarning; - private String[] executableFileNamesList = new String[0]; - ArrayAdapter executableFileNamesAdaptor; - private String[] workingDirectoriesNamesList = new String[0]; - ArrayAdapter workingDirectoriesNamesAdaptor; + private ActivityResultLauncher mStartTextIOActivityForResult; + + private String mStdin; + + private String[] mExecutableFileNamesList = new String[0]; + ArrayAdapter mExecutableFileNamesAdaptor; + private String[] mWorkingDirectoriesNamesList = new String[0]; + ArrayAdapter mWorkingDirectoriesNamesAdaptor; + + public static final String ACTION_GET_STDIN = "ACTION_GET_STDIN"; + + private static final String LOG_TAG = "EditConfigurationActivity"; @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(R.string.app_name); - actionBar.setDisplayHomeAsUpEnabled(true); - } + // Set NightMode.APP_NIGHT_MODE + TermuxThemeUtils.setAppNightMode(this); + AppCompatActivityUtils.setNightMode(this, NightMode.getAppNightMode().getName(), true); + + setContentView(R.layout.activity_edit_configuration); + + AppCompatActivityUtils.setToolbar(this, com.termux.shared.R.id.toolbar); + AppCompatActivityUtils.setToolbarTitle(this, com.termux.shared.R.id.toolbar, TermuxConstants.TERMUX_TASKER_APP_NAME, 0); + AppCompatActivityUtils.setShowBackButtonInActionBar(this, true); - setContentView(R.layout.edit_activity); + setStartTextIOActivityForResult(); final Intent intent = getIntent(); BundleScrubber.scrub(intent); final Bundle localeBundle = intent.getBundleExtra(com.twofortyfouram.locale.Intent.EXTRA_BUNDLE); BundleScrubber.scrub(localeBundle); + Logger.logInfo(LOG_TAG, "Bundle Received: " + IntentUtils.getBundleString(localeBundle)); + TextView mHelp = findViewById(R.id.textview_help); + mHelp.setText(this.getString(R.string.plugin_api_help, TermuxConstants.TERMUX_TASKER_GITHUB_REPO_URL)); + + mExecutablePathTextLayout = findViewById(R.id.layout_executable_path); mExecutablePathText = findViewById(R.id.executable_path); mArgumentsText = findViewById(R.id.arguments); + mWorkingDirectoryPathTextLayout = findViewById(R.id.layout_working_directory_path); mWorkingDirectoryPathText = findViewById(R.id.working_directory_path); + mStdinView = findViewById(R.id.view_stdin); + mSessionActionLayout = findViewById(R.id.layout_session_action); + mSessionAction = findViewById(R.id.session_action); + mBackgroundCustomLogLevelLayout = findViewById(R.id.layout_background_custom_log_level); + mBackgroundCustomLogLevel = findViewById(R.id.background_custom_log_level); mInTerminalCheckbox = findViewById(R.id.in_terminal); + mWaitForResult = findViewById(R.id.wait_for_result); mExecutableAbsolutePathText = findViewById(R.id.executable_absolute_path); mWorkingDirectoryAbsolutePathText = findViewById(R.id.working_directory_absolute_path); mTermuxAppFilesPathInaccessibleWarning = findViewById(R.id.termux_app_files_path_inaccessible_warning); @@ -80,65 +134,80 @@ protected void onCreate(final Bundle savedInstanceState) { mAllowExternalAppsUngrantedWarning = findViewById(R.id.allow_external_apps_ungranted_warning); - mExecutablePathText.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {} + setExecutionPathViews(); + setWorkingDirectoryPathViews(); + setStdinView(); + setSessionActionViews(); + setBackgroundCustomLogLevelViews(); + setInTerminalView(); + + // Currently savedInstanceState bundle is not supported + if (savedInstanceState != null || localeBundle == null) { + Logger.logInfo(LOG_TAG, "Not loading values from null bundle"); + // Enable by default + mInTerminalCheckbox.setChecked(false); + mWaitForResult.setChecked(true); + updateStdinViewVisibility(false); + updateSessionActionViewVisibility(false); + updateBackgroundCustomLogLevelViewVisibility(false); + return; + } - @Override - public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {} + String errmsg; + // If bundle is valid, then load values from bundle + errmsg = PluginBundleManager.parseBundle(this, localeBundle); + if (errmsg != null) { + Logger.logError(LOG_TAG, errmsg); + return; + } - @Override - public void afterTextChanged(Editable editable) { - processExecutablePath(editable == null ? null : editable.toString()); - } - }); + final String selectedExecutable = localeBundle.getString(PluginBundleManager.EXTRA_EXECUTABLE); + mExecutablePathText.setText(selectedExecutable); + processExecutablePath(selectedExecutable); - executableFileNamesAdaptor = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, new ArrayList<>(Arrays.asList(executableFileNamesList))); - mExecutablePathText.setAdapter(executableFileNamesAdaptor); + final String selectedArguments = localeBundle.getString(PluginBundleManager.EXTRA_ARGUMENTS); + mArgumentsText.setText(selectedArguments); + final String selectedWorkingDirectory = localeBundle.getString(PluginBundleManager.EXTRA_WORKDIR); + mWorkingDirectoryPathText.setText(selectedWorkingDirectory); + processWorkingDirectoryPath(selectedWorkingDirectory); - mWorkingDirectoryPathText.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {} + final boolean inTerminal = localeBundle.getBoolean(PluginBundleManager.EXTRA_TERMINAL); + mInTerminalCheckbox.setChecked(inTerminal); - @Override - public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {} + mStdin = DataUtils.getTruncatedCommandOutput(localeBundle.getString(PluginBundleManager.EXTRA_STDIN), + DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, true, false, false); + updateStdinViewText(); + updateStdinViewVisibility(inTerminal); - @Override - public void afterTextChanged(Editable editable) { - processWorkingDirectoryPath(editable == null ? null : editable.toString()); - } - }); + final String sessionAction = localeBundle.getString(PluginBundleManager.EXTRA_SESSION_ACTION); + mSessionAction.setText(sessionAction); + processSessionAction(sessionAction); + updateSessionActionViewVisibility(inTerminal); - workingDirectoriesNamesAdaptor = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, new ArrayList<>(Arrays.asList(workingDirectoriesNamesList))); - mWorkingDirectoryPathText.setAdapter(workingDirectoriesNamesAdaptor); - - - if (savedInstanceState == null) { - if (localeBundle != null) { - String errmsg; - // If bundle is valid, then load values from bundle - errmsg = PluginBundleManager.isBundleValid(this, localeBundle); - if (errmsg == null) { - final String selectedExecutable = localeBundle.getString(PluginBundleManager.EXTRA_EXECUTABLE); - mExecutablePathText.setText(selectedExecutable); - final String selectedArguments = localeBundle.getString(PluginBundleManager.EXTRA_ARGUMENTS); - mArgumentsText.setText(selectedArguments); - final String selectedWorkingDirectory = localeBundle.getString(PluginBundleManager.EXTRA_WORKDIR); - mWorkingDirectoryPathText.setText(selectedWorkingDirectory); - final boolean inTerminal = localeBundle.getBoolean(PluginBundleManager.EXTRA_TERMINAL); - mInTerminalCheckbox.setChecked(inTerminal); - } else { - Logger.logError(this, errmsg); - } - } - } + final String backgroundCustomLogLevel = localeBundle.getString(PluginBundleManager.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL); + mBackgroundCustomLogLevel.setText(backgroundCustomLogLevel); + processBackgroundCustomLogLevel(backgroundCustomLogLevel); + updateBackgroundCustomLogLevelViewVisibility(inTerminal); + + final boolean waitForResult = localeBundle.getBoolean(PluginBundleManager.EXTRA_WAIT_FOR_RESULT, true); + mWaitForResult.setChecked(waitForResult); + } + + @Override + protected void onResume() { + super.onResume(); + + checkIfPluginCanAccessTermuxApp(); + checkIfPluginHostHasPermissionRunCommand(); + processExecutablePath(mExecutablePathText == null ? null : mExecutablePathText.getText().toString()); + processWorkingDirectoryPath(mWorkingDirectoryPathText == null ? null : mWorkingDirectoryPathText.getText().toString()); } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); - getMenuInflater().inflate(R.menu.edit_activity, menu); + getMenuInflater().inflate(R.menu.activity_edit_configuration, menu); return true; } @@ -147,113 +216,133 @@ public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.menu_log_level) { - Logger.showSetLogLevelDialog(this); + LoggerUtils.showSetLogLevelDialog(this); return true; } return super.onOptionsItemSelected(item); } - @Override - public void finish() { - if (!isCanceled()) { - final String executable = mExecutablePathText.getText() == null ? null : mExecutablePathText.getText().toString(); - final String arguments = mArgumentsText.getText() == null ? null : mArgumentsText.getText().toString(); - final String workingDirectory = mWorkingDirectoryPathText.getText() == null ? null : mWorkingDirectoryPathText.getText().toString(); - final boolean inTerminal = mInTerminalCheckbox.isChecked(); - - if (executable != null && executable.length() > 0) { - final Intent resultIntent = new Intent(); - - /* - * This extra is the data to ourselves: either for the Activity or the BroadcastReceiver. Note - * that anything placed in this Bundle must be available to Locale's class loader. So storing - * String, int, and other standard objects will work just fine. Parcelable objects are not - * acceptable, unless they also implement Serializable. Serializable objects must be standard - * Android platform objects (A Serializable class private to this plug-in's APK cannot be - * stored in the Bundle, as Locale's classloader will not recognize it). - */ - final Bundle resultBundle = PluginBundleManager.generateBundle(getApplicationContext(), executable, arguments, workingDirectory, inTerminal); - - // The blurb is a concise status text to be displayed in the host's UI. - final String blurb = generateBlurb(executable, arguments, inTerminal); - - // If host supports variable replacement when running plugin action, then - // request it to replace variables in following fields - if (TaskerPlugin.Setting.hostSupportsOnFireVariableReplacement(this)){ - TaskerPlugin.Setting.setVariableReplaceKeys(resultBundle,new String[] { - PluginBundleManager.EXTRA_EXECUTABLE, - PluginBundleManager.EXTRA_ARGUMENTS, - PluginBundleManager.EXTRA_WORKDIR - }); - } - resultIntent.putExtra(com.twofortyfouram.locale.Intent.EXTRA_BUNDLE, resultBundle); - resultIntent.putExtra(com.twofortyfouram.locale.Intent.EXTRA_STRING_BLURB, blurb); - - // Configuration information for Tasker variables returned from the executed task - // - // Do not run if we are opening a terminal, because the user might not care about this - // if they are running something that will literally pop up in front of them (Plus - // getting that information requires additional work for now) - if(!inTerminal) { - if (TaskerPlugin.hostSupportsRelevantVariables(getIntent().getExtras())) { - TaskerPlugin.addRelevantVariableList(resultIntent, new String[]{ - PluginResultsService.PLUGIN_VARIABLE_STDOUT + "\nStandard Output\nThe stdout of the command.", - PluginResultsService.PLUGIN_VARIABLE_STDERR + "\nStandard Error\nThe stderr of the command.", - PluginResultsService.PLUGIN_VARIABLE_EXIT_CODE + "\nExit Code\nThe exit code of the command. " + - "0 often means success and anything else is usually a failure of some sort." - }); - } - } - // To use variables, we can't have a timeout of 0, but if someone doesn't pay - // attention to this and runs a task that never ends, 10 seconds seems like a - // reasonable timeout. If they need more time, or want this to run entirely - // asynchronously, that can be set - if (TaskerPlugin.Setting.hostSupportsSynchronousExecution(getIntent().getExtras())) { - TaskerPlugin.Setting.requestTimeoutMS(resultIntent, 10000); - } - setResult(RESULT_OK, resultIntent); + + private void setExecutionPathViews() { + mExecutablePathText.addTextChangedListener(new AfterTextChangedWatcher() { + @Override + public void afterTextChanged(Editable editable) { + processExecutablePath(editable == null ? null : editable.toString()); } - } + }); - super.finish(); + mExecutableFileNamesAdaptor = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, new ArrayList<>(Arrays.asList(mExecutableFileNamesList))); + mExecutablePathText.setAdapter(mExecutableFileNamesAdaptor); + } + + + private void setWorkingDirectoryPathViews() { + mWorkingDirectoryPathText.addTextChangedListener(new AfterTextChangedWatcher() { + @Override + public void afterTextChanged(Editable editable) { + processWorkingDirectoryPath(editable == null ? null : editable.toString()); + } + }); + + mWorkingDirectoriesNamesAdaptor = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, new ArrayList<>(Arrays.asList(mWorkingDirectoriesNamesList))); + mWorkingDirectoryPathText.setAdapter(mWorkingDirectoriesNamesAdaptor); } - /** - * The message that will be displayed by the plugin host app for the action configuration. - * Blurb length can be a maximum of 60 characters as defined by locale lib. - * @param executable value set for the action. - * @param arguments value set for the action. - * @param inTerminal value set for the action. - * @return A blurb for the plug-in. - */ - String generateBlurb(final String executable, final String arguments, boolean inTerminal) { - final int stringResource = inTerminal ? R.string.blurb_in_terminal : R.string.blurb_in_background; - final String message = getString(stringResource, executable, arguments); - final int maxBlurbLength = 60; // R.integer.twofortyfouram_locale_maximum_blurb_length. - return (message.length() > maxBlurbLength) ? message.substring(0, maxBlurbLength) : message; + + private void setStdinView() { + mStdinView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mStartTextIOActivityForResult == null) return; + + // Tasker has a Bundle size limit check: + // Parcel obtain = Parcel.obtain(); bundle.writeToParcel(obtain, 0); if (obtain.dataSize() < 100000); + // And will throw `plugin data too large` if it exceeds. + // Note that on Android 7-11, String characters are stored in Parcel as UTF-16, i.e 2 bytes + // https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/core/java/android/os/Parcel.java;l=773 + TextIOInfo textIOInfo = new TextIOInfo(ACTION_GET_STDIN, EditConfigurationActivity.this.getClass().getCanonicalName()); + textIOInfo.setTitle(getString(R.string.title_stdin)); + textIOInfo.setText(mStdin); + textIOInfo.setTextSize(12); + textIOInfo.setTextLengthLimit(90000/2); // Leave some for other data in result bundle. Limit is 45K characters. + textIOInfo.setTextTypeFaceFamily("monospace"); + textIOInfo.setTextTypeFaceStyle(Typeface.NORMAL); + textIOInfo.setTextHorizontallyScrolling(true); + textIOInfo.setShowTextCharacterUsage(true); + textIOInfo.setShowBackButtonInActionBar(true); + + mStartTextIOActivityForResult.launch(TextIOActivity.newInstance(EditConfigurationActivity.this, textIOInfo)); + } + }); } - @Override - protected void onResume() { - super.onResume(); + private void updateStdinViewText() { + if (mStdinView == null) return; + mStdinView.setText(DataUtils.getTruncatedCommandOutput(mStdin, 200, true, false, false)); + } - checkIfPluginCanAccessTermuxApp(); - checkIfPluginHostHasPermissionRunCommand(); - processExecutablePath(mExecutablePathText == null ? null : mExecutablePathText.getText().toString()); - processWorkingDirectoryPath(mWorkingDirectoryPathText == null ? null : mWorkingDirectoryPathText.getText().toString()); + private void updateStdinViewVisibility(boolean inTerminal) { + if (mStdinView == null) return; + mStdinView.setVisibility(inTerminal ? View.GONE : View.VISIBLE); + } + + + private void setSessionActionViews() { + mSessionAction.addTextChangedListener(new AfterTextChangedWatcher() { + @Override + public void afterTextChanged(Editable editable) { + processSessionAction(editable == null ? null : editable.toString()); + } + }); + } + + private void updateSessionActionViewVisibility(boolean inTerminal) { + if (mSessionAction == null) return; + mSessionAction.setVisibility(inTerminal ? View.VISIBLE : View.GONE); + } + + + private void setBackgroundCustomLogLevelViews() { + mBackgroundCustomLogLevel.addTextChangedListener(new AfterTextChangedWatcher() { + @Override + public void afterTextChanged(Editable editable) { + processBackgroundCustomLogLevel(editable == null ? null : editable.toString()); + } + }); } + private void updateBackgroundCustomLogLevelViewVisibility(boolean inTerminal) { + if (mBackgroundCustomLogLevel == null) return; + mBackgroundCustomLogLevel.setVisibility(inTerminal ? View.GONE : View.VISIBLE); + } + + + private void setInTerminalView() { + mInTerminalCheckbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + updateStdinViewVisibility(isChecked); + updateSessionActionViewVisibility(isChecked); + updateBackgroundCustomLogLevelViewVisibility(isChecked); + } + }); + } + + + private void checkIfPluginCanAccessTermuxApp() { if (mTermuxAppFilesPathInaccessibleWarning == null) return; String errmsg; - // If Termux app is not installed or PREFIX_PATH is not accessible, then show warning - errmsg = TermuxAppUtils.checkIfTermuxAppIsInstalledAndAccessible(this); + // If Termux app is not installed, enabled or accessible with current context or if + // TermuxConstants.TERMUX_PREFIX_DIR_PATH does not exist or has required permissions, + // then show warning. + errmsg = TermuxUtils.isTermuxAppAccessible(this); if (errmsg != null) { mTermuxAppFilesPathInaccessibleWarning.setText(errmsg); mTermuxAppFilesPathInaccessibleWarning.setVisibility(View.VISIBLE); @@ -286,43 +375,45 @@ private void processExecutablePath(String executable) { boolean validate = true; boolean executableDefined = true; - mExecutablePathText.setError(null); + mExecutablePathTextLayout.setError(null); mExecutableAbsolutePathText.setText(null); mAllowExternalAppsUngrantedWarning.setVisibility(View.GONE); mAllowExternalAppsUngrantedWarning.setText(null); if (executable == null || executable.isEmpty()) { - mExecutablePathText.setError(this.getString(R.string.executable_required)); + mExecutablePathTextLayout.setError(this.getString(R.string.error_executable_required)); validate = false; executableDefined = false; } - executable = FileUtils.getAbsolutePathForExecutable(executable); + executable = TermuxFileUtils.getCanonicalPath(executable, TermuxConstants.TERMUX_TASKER_SCRIPTS_DIR_PATH, true); // If executable text contains a variable, then no need to set absolute path or validate the path - if (PluginResultsService.isPluginHostAppVariableContainingString(executable)) { - mExecutableAbsolutePathText.setText(this.getString(R.string.executable_absolute_path, this.getString(R.string.variable_in_path))); + if (PluginUtils.isPluginHostAppVariableContainingString(executable)) { + mExecutableAbsolutePathText.setText(this.getString(R.string.msg_absolute_path, this.getString(R.string.msg_variable_in_string))); executable = null; validate = false; } else if (executableDefined) { - mExecutableAbsolutePathText.setText(this.getString(R.string.executable_absolute_path, executable)); + mExecutableAbsolutePathText.setText(this.getString(R.string.msg_absolute_path, executable)); } if (validate) { - File executableFile = new File(executable); - - String errmsg; - - // If executable is not a file, cannot be read or be executed, then return RESULT_CODE_FAILED to plugin host app - // Readable and executable checks are to be ignored if path is in TASKER_PATH since FireReceiver will automatically - // set read and execute permissions on execution - errmsg = FileUtils.checkIfExecutableFileIsReadableAndExecutable(this, executable, false, true); - if (errmsg != null) { - mExecutablePathText.setError(errmsg); + // If executable is not a file, cannot be read or be executed, then show warning + // Missing permissions (but not existence) checks are to be ignored if path is in + // TermuxConstants#TERMUX_TASKER_SCRIPTS_DIR_PATH since FireReceiver + // will automatically set permissions on execution. + Error error = FileUtils.validateRegularFileExistenceAndPermissions("executable", executable, + TermuxConstants.TERMUX_TASKER_SCRIPTS_DIR_PATH, + FileUtils.APP_EXECUTABLE_FILE_PERMISSIONS,false, false, + true); + if (error != null) { + Error shortError = FileUtils.getShortFileUtilsError(error); + mExecutablePathTextLayout.setError(shortError.getMessage()); } - // If executable is not in TASKER_PATH and allow-external-apps property to not set to "true", then show warning - errmsg = PluginUtils.checkIfAllowExternalAppsPolicyIsViolated(this, executable); + // If executable is not in TermuxConstants#TERMUX_TASKER_SCRIPTS_DIR_PATH and + // "allow-external-apps" property to not set to "true", then show warning + String errmsg = PluginUtils.checkIfTermuxTaskerAllowExternalAppsPolicyIsViolated(this, executable); if (errmsg != null) { mAllowExternalAppsUngrantedWarning.setText(errmsg); mAllowExternalAppsUngrantedWarning.setVisibility(View.VISIBLE); @@ -346,13 +437,15 @@ private void setExecutablePathTextDropdownList(String executable) { String executablePathText = mExecutablePathText.getText().toString(); - // If executable is null, empty or executable parent is not a directory, then show files in TASKER_DIR + // If executable is null, empty or executable parent is not a directory, then show files + // in TermuxConstants.TERMUX_TASKER_SCRIPTS_DIR if (executable == null || executable.isEmpty() || executableParentFile == null || !executableParentFile.isDirectory()) { - files = Constants.TASKER_DIR.listFiles(); + files = TermuxConstants.TERMUX_TASKER_SCRIPTS_DIR.listFiles(); } - // If executable is a substring of FILES_PATH, then show files in FILES_PATH - else if (Constants.FILES_PATH.contains(executable)) { - executableParentFile = new File(Constants.FILES_PATH); + // If executable is a substring of TermuxConstants.TERMUX_FILES_DIR_PATH, then show + // files in TermuxConstants.TERMUX_FILES_DIR_PATH + else if (TermuxConstants.TERMUX_FILES_DIR_PATH.contains(executable)) { + executableParentFile = new File(TermuxConstants.TERMUX_FILES_DIR_PATH); files = executableParentFile.listFiles(); } // If executable path in text field ends with "/", then show files in current directory instead of parent directory @@ -365,8 +458,8 @@ else if (executablePathText.endsWith("/")) { files = executableParentFile.listFiles(); } - //Logger.logVerbose(this, "executable: " + executable); - //Logger.logVerbose(this, "executablePathText: " + executablePathText); + //Logger.logVerbose(LOG_TAG, "executable: " + executable); + //Logger.logVerbose(LOG_TAG, "executablePathText: " + executablePathText); if (files != null && files.length > 0) { Arrays.sort(files); @@ -374,33 +467,35 @@ else if (executablePathText.endsWith("/")) { // If executable is not null, empty or executable parent is not null if (executable != null && !executable.isEmpty() && executableParentFile != null) { String executableParentPath = executableParentFile.getAbsolutePath(); - //Logger.logVerbose(this, "executableParentPath: " + executableParentPath); + //Logger.logVerbose(LOG_TAG, "executableParentPath: " + executableParentPath); - // If executable path in text field starts with "/", then prefix file names with the parent directory in the drop down list + // If executable path in text field starts with "/", then prefix file names with the + // parent directory in the drop down list if (executablePathText.startsWith("/")) executableFileNamesPrefix = executableParentPath + "/"; - // If executable path in text field starts with "$PREFIX/" or "~/", then prefix file names with the unexpanded path in the drop down list + // If executable path in text field starts with "$PREFIX/" or "~/", then prefix file names + // with the unexpanded path in the drop down list else if (executablePathText.startsWith("$PREFIX/") || executablePathText.startsWith("~/")) { - executableFileNamesPrefix = FileUtils.getUnExpandedTermuxPath(executableParentPath + "/"); + executableFileNamesPrefix = TermuxFileUtils.getUnExpandedTermuxPath(executableParentPath + "/"); } } // Create a string array of filenames with the optional prefix for the drop down list - executableFileNamesList = new String[files.length]; + mExecutableFileNamesList = new String[files.length]; for (int i = 0; i < files.length; i++) { - executableFileNamesList[i] = executableFileNamesPrefix + files[i].getName(); + mExecutableFileNamesList[i] = executableFileNamesPrefix + files[i].getName(); } } else { - executableFileNamesList = new String[0]; + mExecutableFileNamesList = new String[0]; } - //Logger.logVerbose(this, Arrays.toString(executableFileNamesList)); + //Logger.logVerbose(LOG_TAG, "mExecutableFileNamesList: " + Arrays.toString(mExecutableFileNamesList)); // Update drop down list and show it - executableFileNamesAdaptor.clear(); - executableFileNamesAdaptor.addAll(new ArrayList<>(Arrays.asList(executableFileNamesList))); - executableFileNamesAdaptor.notifyDataSetChanged(); - if (mExecutablePathText.isFocused()) + mExecutableFileNamesAdaptor.clear(); + mExecutableFileNamesAdaptor.addAll(new ArrayList<>(Arrays.asList(mExecutableFileNamesList))); + mExecutableFileNamesAdaptor.notifyDataSetChanged(); + if (mExecutablePathText.isFocused() && mExecutablePathText.getWindowToken() != null) mExecutablePathText.showDropDown(); } @@ -410,7 +505,7 @@ private void processWorkingDirectoryPath(String workingDirectory) { boolean validate = true; boolean workingDirectoryDefined = true; - mWorkingDirectoryPathText.setError(null); + mWorkingDirectoryPathTextLayout.setError(null); mWorkingDirectoryAbsolutePathText.setVisibility(View.GONE); mWorkingDirectoryAbsolutePathText.setText(null); @@ -419,30 +514,30 @@ private void processWorkingDirectoryPath(String workingDirectory) { workingDirectoryDefined = false; } - workingDirectory = FileUtils.getAbsolutePathForExecutable(workingDirectory); + workingDirectory = TermuxFileUtils.getCanonicalPath(workingDirectory, null, true); // If workingDirectory text contains a variable, then no need to set absolute path or validate the path - if (PluginResultsService.isPluginHostAppVariableContainingString(workingDirectory)) { - mWorkingDirectoryAbsolutePathText.setText(this.getString(R.string.working_directory_absolute_path, this.getString(R.string.variable_in_path))); + if (PluginUtils.isPluginHostAppVariableContainingString(workingDirectory)) { + mWorkingDirectoryAbsolutePathText.setText(this.getString(R.string.msg_absolute_path, this.getString(R.string.msg_variable_in_string))); mWorkingDirectoryAbsolutePathText.setVisibility(View.VISIBLE); workingDirectory = null; validate = false; } else if (workingDirectoryDefined) { - mWorkingDirectoryAbsolutePathText.setText(this.getString(R.string.working_directory_absolute_path, workingDirectory)); + mWorkingDirectoryAbsolutePathText.setText(this.getString(R.string.msg_absolute_path, workingDirectory)); mWorkingDirectoryAbsolutePathText.setVisibility(View.VISIBLE); } if (validate) { - File executableFile = new File(workingDirectory); - - String errmsg; - - // If workingDirectory is not a directory or cannot be read, then return RESULT_CODE_FAILED to plugin host app - // Existence and readable checks are to be ignored if path is in HOME_PATH since FireReceiver will automatically - // create and set read permissions on execution - errmsg = FileUtils.checkIfDirectoryIsReadable(this, workingDirectory, false, false, true); - if (errmsg != null) { - mWorkingDirectoryPathText.setError(errmsg); + // If workingDirectory is not a directory or cannot be read, then show warning + // Existence and missing permissions checks are to be ignored if path is under allowed + // termux working directory paths since FireReceiver will automatically create and set + // permissions on execution. + Error error = TermuxFileUtils.validateDirectoryFileExistenceAndPermissions("working", workingDirectory, + false, false, false, + false, true); + if (error != null) { + Error shortError = FileUtils.getShortFileUtilsError(error); + mWorkingDirectoryPathTextLayout.setError(shortError.getMessage()); } } @@ -464,15 +559,17 @@ private void setWorkingDirectoryPathTextDropdownList(String workingDirectory) { String workingDirectoryPathText = mWorkingDirectoryPathText.getText().toString(); // If workingDirectory is null, empty or workingDirectory parent is not a directory, then show nothing - if (workingDirectory == null || workingDirectory.isEmpty() || workingDirectoryParentFile == null || !workingDirectoryParentFile.isDirectory()) { + if (workingDirectory == null || workingDirectory.isEmpty() || + workingDirectoryParentFile == null || !workingDirectoryParentFile.isDirectory()) { files = new File[0]; } // If workingDirectory is a substring of FILES_PATH, then show files in FILES_PATH - else if (Constants.FILES_PATH.contains(workingDirectory)) { - workingDirectoryParentFile = new File(Constants.FILES_PATH); + else if (TermuxConstants.TERMUX_FILES_DIR_PATH.contains(workingDirectory)) { + workingDirectoryParentFile = TermuxConstants.TERMUX_FILES_DIR; files = workingDirectoryParentFile.listFiles(); } - // If workingDirectory path in text field ends with "/", then show files in current directory instead of parent directory + // If workingDirectory path in text field ends with "/", then show files in current directory + // instead of parent directory else if (workingDirectoryPathText.endsWith("/")) { workingDirectoryParentFile = workingDirectoryFile; files = workingDirectoryParentFile.listFiles(); @@ -482,8 +579,8 @@ else if (workingDirectoryPathText.endsWith("/")) { files = workingDirectoryParentFile.listFiles(); } - //Logger.logVerbose(this, "workingDirectory: " + workingDirectory); - //Logger.logVerbose(this, "workingDirectoryPathText: " + workingDirectoryPathText); + //Logger.logVerbose(LOG_TAG, "workingDirectory: " + workingDirectory); + //Logger.logVerbose(LOG_TAG, "workingDirectoryPathText: " + workingDirectoryPathText); if (files != null && files.length > 0) { Arrays.sort(files); @@ -491,33 +588,202 @@ else if (workingDirectoryPathText.endsWith("/")) { // If workingDirectory is not null, empty or workingDirectory parent is not null String workingDirectoryParentPath = workingDirectoryParentFile.getAbsolutePath(); - //Logger.logVerbose(this, "workingDirectoryParentPath: " + workingDirectoryParentPath); + //Logger.logVerbose(LOG_TAG, "workingDirectoryParentPath: " + workingDirectoryParentPath); - // If workingDirectory path in text field starts with "/", then prefix file names with the parent directory in the drop down list + // If workingDirectory path in text field starts with "/", then prefix file names with the + // parent directory in the drop down list if (workingDirectoryPathText.startsWith("/")) workingDirectoryFileNamesPrefix = workingDirectoryParentPath + "/"; - // If workingDirectory path in text field starts with "$PREFIX/" or "~/", then prefix file names with the unexpanded path in the drop down list + // If workingDirectory path in text field starts with "$PREFIX/" or "~/", then prefix + // file names with the unexpanded path in the drop down list else if (workingDirectoryPathText.startsWith("$PREFIX/") || workingDirectoryPathText.startsWith("~/")) { - workingDirectoryFileNamesPrefix = FileUtils.getUnExpandedTermuxPath(workingDirectoryParentPath + "/"); + workingDirectoryFileNamesPrefix = TermuxFileUtils.getUnExpandedTermuxPath(workingDirectoryParentPath + "/"); } // Create a string array of filenames with the optional prefix for the drop down list - workingDirectoriesNamesList = new String[files.length]; + mWorkingDirectoriesNamesList = new String[files.length]; for (int i = 0; i < files.length; i++) { - workingDirectoriesNamesList[i] = workingDirectoryFileNamesPrefix + files[i].getName(); + mWorkingDirectoriesNamesList[i] = workingDirectoryFileNamesPrefix + files[i].getName(); } } else { - workingDirectoriesNamesList = new String[0]; + mWorkingDirectoriesNamesList = new String[0]; } - //Logger.logVerbose(this, Arrays.toString(workingDirectoriesNamesList)); + //Logger.logVerbose(LOG_TAG, "mWorkingDirectoriesNamesList: " + Arrays.toString(mWorkingDirectoriesNamesList)); // Update drop down list and show it - workingDirectoriesNamesAdaptor.clear(); - workingDirectoriesNamesAdaptor.addAll(new ArrayList<>(Arrays.asList(workingDirectoriesNamesList))); - workingDirectoriesNamesAdaptor.notifyDataSetChanged(); - if (mWorkingDirectoryPathText.isFocused()) + mWorkingDirectoriesNamesAdaptor.clear(); + mWorkingDirectoriesNamesAdaptor.addAll(new ArrayList<>(Arrays.asList(mWorkingDirectoriesNamesList))); + mWorkingDirectoriesNamesAdaptor.notifyDataSetChanged(); + if (mWorkingDirectoryPathText.isFocused() && mWorkingDirectoryPathText.getWindowToken() != null) mWorkingDirectoryPathText.showDropDown(); } + private void processSessionAction(String sessionActionString) { + processIntFieldValue(mSessionActionLayout, sessionActionString, + TERMUX_SERVICE.MIN_VALUE_EXTRA_SESSION_ACTION, TERMUX_SERVICE.MAX_VALUE_EXTRA_SESSION_ACTION); + } + + private void processBackgroundCustomLogLevel(String backgroundCustomLogLevelString) { + processIntFieldValue(mBackgroundCustomLogLevelLayout, backgroundCustomLogLevelString, + Logger.LOG_LEVEL_OFF, Logger.MAX_LOG_LEVEL); + } + + private void processIntFieldValue(TextInputLayout editText, String stringValue, int min, int max) { + if (editText == null) return; + editText.setError(null); + if (DataUtils.isNullOrEmpty(stringValue)) return; + if (PluginUtils.isPluginHostAppVariableContainingString(stringValue)) return; + + Integer value = null; + boolean invalid = false; + + try { + value = Integer.parseInt(stringValue); + } + catch (Exception e) { + invalid = true; + } + + if (invalid || value < min || value > max) { + editText.setError(getString(R.string.error_int_not_in_range, min, max)); + } + } + + + + + + @Override + public void finish() { + if (isCanceled()) { + super.finish(); + return; + } + + final String executable = DataUtils.getDefaultIfUnset(mExecutablePathText.getText() == null ? null : mExecutablePathText.getText().toString(), null); + final String arguments = DataUtils.getDefaultIfUnset(mArgumentsText.getText() == null ? null : mArgumentsText.getText().toString(), null); + final String workingDirectory = DataUtils.getDefaultIfUnset(mWorkingDirectoryPathText.getText() == null ? null : mWorkingDirectoryPathText.getText().toString(), null); + final String sessionAction = DataUtils.getDefaultIfUnset(mSessionAction.getText() == null ? null : mSessionAction.getText().toString(), null); + final String backgroundCustomLogLevel = DataUtils.getDefaultIfUnset(mBackgroundCustomLogLevel.getText() == null ? null : mBackgroundCustomLogLevel.getText().toString(), null); + final boolean inTerminal = mInTerminalCheckbox.isChecked(); + final boolean waitForResult = mWaitForResult.isChecked(); + + if (executable == null || executable.length() <= 0) { + super.finish(); + return; + } + + final Intent resultIntent = new Intent(); + + /* + * This extra is the data to ourselves: either for the Activity or the BroadcastReceiver. Note + * that anything placed in this Bundle must be available to Locale's class loader. So storing + * String, int, and other standard objects will work just fine. Parcelable objects are not + * acceptable, unless they also implement Serializable. Serializable objects must be standard + * Android platform objects (A Serializable class private to this plug-in's APK cannot be + * stored in the Bundle, as Locale's classloader will not recognize it). + */ + final Bundle resultBundle = PluginBundleManager.generateBundle(getApplicationContext(), + executable, arguments, workingDirectory, mStdin, sessionAction, backgroundCustomLogLevel, inTerminal, waitForResult); + if (resultBundle == null) { + Logger.showToast(this, getString(R.string.error_generate_plugin_bundle_failed), true); + setResult(RESULT_CODE_FAILED, resultIntent); + super.finish(); + return; + } + + Logger.logDebug(LOG_TAG, "Result bundle size: " + PluginBundleManager.getBundleSize(resultBundle)); + + // The blurb is a concise status text to be displayed in the host's UI. + final String blurb = PluginBundleManager.generateBlurb(this, executable, arguments, + workingDirectory, mStdin, sessionAction, backgroundCustomLogLevel, inTerminal, waitForResult); + + // If host supports variable replacement when running plugin action, then + // request it to replace variables in following fields + if (TaskerPlugin.Setting.hostSupportsOnFireVariableReplacement(this)){ + TaskerPlugin.Setting.setVariableReplaceKeys(resultBundle,new String[] { + PluginBundleManager.EXTRA_EXECUTABLE, + PluginBundleManager.EXTRA_ARGUMENTS, + PluginBundleManager.EXTRA_WORKDIR, + PluginBundleManager.EXTRA_STDIN, + PluginBundleManager.EXTRA_SESSION_ACTION, + PluginBundleManager.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL + }); + } + + resultIntent.putExtra(com.twofortyfouram.locale.Intent.EXTRA_BUNDLE, resultBundle); + resultIntent.putExtra(com.twofortyfouram.locale.Intent.EXTRA_STRING_BLURB, blurb); + + // Configuration information for Tasker variables returned from the executed task + if(waitForResult) { + List relevantVariableList = new ArrayList<>(); + relevantVariableList.add(PluginUtils.PLUGIN_VARIABLE_STDOUT + "\nStandard Output\nThe stdout of the command."); + relevantVariableList.add(PluginUtils.PLUGIN_VARIABLE_STDOUT_ORIGINAL_LENGTH + "\nStandard Output Original Length\nThe original length of stdout."); + + // For foreground commands, the session transcript is returned which will contain + // both stdout and stderr combined, basically anything sent to the the pseudo + // terminal /dev/pts, including PS1 prefixes for interactive sessions. + if (!inTerminal) { + relevantVariableList.add(PluginUtils.PLUGIN_VARIABLE_STDERR + "\nStandard Error\nThe stderr of the command."); + relevantVariableList.add(PluginUtils.PLUGIN_VARIABLE_STDERR_ORIGINAL_LENGTH + "\nStandard Error Original Length\nThe original length of stderr."); + } + + relevantVariableList.add(PluginUtils.PLUGIN_VARIABLE_EXIT_CODE + "\nExit Code\nThe exit code of the command." + + "0 often means success and anything else is usually a failure of some sort."); + + if (TaskerPlugin.hostSupportsRelevantVariables(getIntent().getExtras())) { + TaskerPlugin.addRelevantVariableList(resultIntent, relevantVariableList.toArray(new String[0])); + } + } + + // To use variables, we can't have a timeout of 0, but if someone doesn't pay + // attention to this and runs a task that never ends, 10 seconds seems like a + // reasonable timeout. If they need more time, or want this to run entirely + // asynchronously, that can be set + if (TaskerPlugin.Setting.hostSupportsSynchronousExecution(getIntent().getExtras())) { + TaskerPlugin.Setting.requestTimeoutMS(resultIntent, 10000); + } + + setResult(RESULT_OK, resultIntent); + super.finish(); + } + + + + + + private void setStartTextIOActivityForResult() { + mStartTextIOActivityForResult = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + if (result.getResultCode() == Activity.RESULT_OK) { + Intent intent = result.getData(); + if (intent == null) return; + + Bundle bundle = intent.getExtras(); + if (bundle == null) return; + + TextIOInfo textIOInfo = (TextIOInfo) bundle.getSerializable(TextIOActivity.EXTRA_TEXT_IO_INFO_OBJECT); + if (textIOInfo == null) return; + + switch (textIOInfo.getAction()) { + case ACTION_GET_STDIN: + mStdin = textIOInfo.getText(); + updateStdinViewText(); + } + } + }); + } + + static class AfterTextChangedWatcher implements TextWatcher { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {} + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {} + + @Override + public void afterTextChanged(Editable editable) { + } + } + } diff --git a/app/src/main/java/com/termux/tasker/FireReceiver.java b/app/src/main/java/com/termux/tasker/FireReceiver.java index 58f2701..dd0e3c5 100644 --- a/app/src/main/java/com/termux/tasker/FireReceiver.java +++ b/app/src/main/java/com/termux/tasker/FireReceiver.java @@ -6,11 +6,23 @@ import android.net.Uri; import android.os.Bundle; -import com.termux.tasker.utils.FileUtils; -import com.termux.tasker.utils.Logger; +import com.termux.shared.data.DataUtils; +import com.termux.shared.data.IntentUtils; +import com.termux.shared.errors.Errno; +import com.termux.shared.errors.Error; +import com.termux.shared.file.filesystem.FileType; +import com.termux.shared.logger.Logger; +import com.termux.shared.shell.command.ExecutionCommand; +import com.termux.shared.termux.TermuxConstants; +import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE; +import com.termux.shared.file.FileUtils; +import com.termux.shared.termux.TermuxUtils; +import com.termux.shared.termux.crash.TermuxCrashUtils; +import com.termux.shared.termux.file.TermuxFileUtils; import com.termux.tasker.utils.PluginUtils; -import com.termux.tasker.utils.TermuxAppUtils; +import com.termux.tasker.utils.TaskerPlugin; +import java.util.ArrayList; import java.util.List; /** @@ -18,126 +30,172 @@ */ public final class FireReceiver extends BroadcastReceiver { - public static final String ACTION_EXECUTE = "com.termux.service_execute"; - public static final String EXTRA_CURRENT_WORKING_DIRECTORY = "com.termux.execute.cwd"; - public static final String EXTRA_EXECUTE_IN_BACKGROUND = "com.termux.execute.background"; + private static final String LOG_TAG = "FireReceiver"; public void onReceive(final Context context, final Intent intent) { - // Load the log level value from shared preferences file into the SharedPreferencesImpl - // memory cache if file was updated by the main app process + // Set crash handler for the receiver + TermuxCrashUtils.setCrashHandler(context); + + // Load the log level from shared preferences and set it to the Logger.CURRENT_LOG_LEVEL // Till the onReceive() function is called again, any changes to log level will not be considered - // since log level will be loaded from memory cache by Logger.getLogLevel() for checking - // current log level by all the logging functions - // Log levels can also be read from the file if updated, but that will slow down execution, - // hence that is not done - Logger.getLogLevelFromFile(context); + // since log level will be stored in the Loggger.CURRENT_LOG_LEVEL static variable. + // The FireReceiver is started in a separate process than the main app process since + // it has the tag android:process=":background" and so it maintains separate Logger and + // shared preference instances. + TermuxTaskerApplication.setLogConfig(context, false); // If wrong action passed, then just return if (!com.twofortyfouram.locale.Intent.ACTION_FIRE_SETTING.equals(intent.getAction())) { - Logger.logError(context, "Unexpected intent action: " + intent.getAction()); + Logger.logError(LOG_TAG, "Unexpected intent action: " + intent.getAction()); return; } - Logger.logInfo(context, "FireReceiver received execution intent"); + Logger.logInfo(LOG_TAG, "Received execution intent"); String errmsg; + Error error; BundleScrubber.scrub(intent); final Bundle bundle = intent.getBundleExtra(com.twofortyfouram.locale.Intent.EXTRA_BUNDLE); BundleScrubber.scrub(bundle); // If bundle is not valid, then return RESULT_CODE_FAILED to plugin host app - errmsg = PluginBundleManager.isBundleValid(context, bundle); + errmsg = PluginBundleManager.parseBundle(context, bundle); if (errmsg != null) { - Logger.logError(context, errmsg); - PluginResultsService.sendImmediateResultToPluginHostApp(context, this, intent, null, null, null, TaskerPlugin.Setting.RESULT_CODE_FAILED, errmsg); + Logger.logError(LOG_TAG, errmsg); + PluginUtils.sendImmediateResultToPluginHostApp(this, intent, TaskerPlugin.Setting.RESULT_CODE_FAILED, errmsg); return; } - String executable = bundle.getString(PluginBundleManager.EXTRA_EXECUTABLE); + ExecutionCommand executionCommand = new ExecutionCommand(); + + String executableExtra = executionCommand.executable = IntentUtils.getStringExtraIfSet(intent, PluginBundleManager.EXTRA_EXECUTABLE, null); final String arguments_string = bundle.getString(PluginBundleManager.EXTRA_ARGUMENTS); - String workingDirectory = bundle.getString(PluginBundleManager.EXTRA_WORKDIR); - final boolean inTerminal = bundle.getBoolean(PluginBundleManager.EXTRA_TERMINAL); + executionCommand.workingDirectory = IntentUtils.getStringExtraIfSet(intent, PluginBundleManager.EXTRA_WORKDIR, null); + executionCommand.runner = intent.getBooleanExtra(PluginBundleManager.EXTRA_TERMINAL, false) ? + ExecutionCommand.Runner.TERMINAL_SESSION.getName() : ExecutionCommand.Runner.APP_SHELL.getName(); + final boolean waitForResult = bundle.getBoolean(PluginBundleManager.EXTRA_WAIT_FOR_RESULT, true); + + if (ExecutionCommand.Runner.APP_SHELL.equalsRunner(executionCommand.runner)) { + executionCommand.stdin = IntentUtils.getStringExtraIfSet(intent, PluginBundleManager.EXTRA_STDIN, null); + executionCommand.backgroundCustomLogLevel = IntentUtils.getIntegerExtraIfSet(intent, PluginBundleManager.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL, null); + } else { + executionCommand.sessionAction = IntentUtils.getStringExtraIfSet(intent, PluginBundleManager.EXTRA_SESSION_ACTION, null); + } - // If Termux app is not installed or PREFIX_PATH is not accessible, then return RESULT_CODE_FAILED to plugin host app - errmsg = TermuxAppUtils.checkIfTermuxAppIsInstalledAndAccessible(context); + // If Termux app is not installed, enabled or accessible with current context or if + // TermuxConstants.TERMUX_PREFIX_DIR_PATH does not exist or has required permissions, then + // return RESULT_CODE_FAILED to plugin host app. + errmsg = TermuxUtils.isTermuxAppAccessible(context); if (errmsg != null) { - Logger.logError(context, errmsg); - PluginResultsService.sendImmediateResultToPluginHostApp(context, this, intent, null, null, null, TaskerPlugin.Setting.RESULT_CODE_FAILED, errmsg); + Logger.logError(LOG_TAG, errmsg); + PluginUtils.sendImmediateResultToPluginHostApp(this, intent, TaskerPlugin.Setting.RESULT_CODE_FAILED, errmsg); return; } - // Get absolute canonical path of executable - executable = FileUtils.getAbsolutePathForExecutable(executable); - // If executable is not in TASKER_PATH and allow-external-apps property to not set to "true", then return RESULT_CODE_FAILED to plugin host app - errmsg = PluginUtils.checkIfAllowExternalAppsPolicyIsViolated(context, executable); - if (errmsg != null) { - errmsg += "\n" + context.getString(R.string.executable_absolute_path, executable) + - "\n" + context.getString(R.string.help); - Logger.logError(context, errmsg); - PluginResultsService.sendImmediateResultToPluginHostApp(context, this, intent, null, null, null, TaskerPlugin.Setting.RESULT_CODE_FAILED, errmsg); + // 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 = context.getString(R.string.error_null_or_empty_executable); + Logger.logError(LOG_TAG, errmsg); + PluginUtils.sendImmediateResultToPluginHostApp(this, intent, TaskerPlugin.Setting.RESULT_CODE_FAILED, errmsg); return; } - // If executable is not a file, cannot be read or be executed, then return RESULT_CODE_FAILED to plugin host app - errmsg = FileUtils.checkIfExecutableFileIsReadableAndExecutable(context, executable, true, false); + // Get canonical path of executable + executionCommand.executable = TermuxFileUtils.getCanonicalPath(executionCommand.executable, TermuxConstants.TERMUX_TASKER_SCRIPTS_DIR_PATH, true); + + + // If executable is not in TermuxConstants#TERMUX_TASKER_SCRIPTS_DIR_PATH and + // "allow-external-apps" property to not set to "true", then return RESULT_CODE_FAILED to plugin host app + errmsg = PluginUtils.checkIfTermuxTaskerAllowExternalAppsPolicyIsViolated(context, executionCommand.executable); if (errmsg != null) { - errmsg += "\n" + context.getString(R.string.executable_absolute_path, executable); - Logger.logError(context, errmsg); - PluginResultsService.sendImmediateResultToPluginHostApp(context, this, intent, null, null, null, TaskerPlugin.Setting.RESULT_CODE_FAILED, errmsg); + errmsg += "\n" + context.getString(R.string.msg_executable_absolute_path, executionCommand.executable) + + "\n" + context.getString(R.string.plugin_api_help, TermuxConstants.TERMUX_TASKER_GITHUB_REPO_URL); + executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg); + PluginUtils.processPluginExecutionCommandError(context, this, intent, LOG_TAG, executionCommand, TaskerPlugin.Setting.RESULT_CODE_FAILED); + return; + } + + + // If executable is not a regular file, or is not readable or executable, then return + // RESULT_CODE_FAILED to plugin host app + // Setting of read and execute permissions are only done if executable is under TermuxConstants#TERMUX_TASKER_SCRIPTS_DIR_PATH + error = FileUtils.validateRegularFileExistenceAndPermissions("executable", executionCommand.executable, + TermuxConstants.TERMUX_TASKER_SCRIPTS_DIR_PATH, + FileUtils.APP_EXECUTABLE_FILE_PERMISSIONS, + true, true, + false); + if (error != null) { + executionCommand.setStateFailed(error); + PluginUtils.processPluginExecutionCommandError(context, this, intent, LOG_TAG, executionCommand, TaskerPlugin.Setting.RESULT_CODE_FAILED); return; } + // If workingDirectory is not null or empty - if (workingDirectory != null && !workingDirectory.isEmpty()) { - // Get absolute canonical path of executable - workingDirectory = FileUtils.getAbsolutePathForExecutable(workingDirectory); - - // If workingDirectory is not a directory or cannot be read, then return RESULT_CODE_FAILED to plugin host app - errmsg = FileUtils.checkIfDirectoryIsReadable(context, workingDirectory, true, true, false); - if (errmsg != null) { - errmsg += "\n" + context.getString(R.string.working_directory_absolute_path, workingDirectory); - Logger.logError(context, errmsg); - PluginResultsService.sendImmediateResultToPluginHostApp(context, this, intent, null, null, null, TaskerPlugin.Setting.RESULT_CODE_FAILED, errmsg); + if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) { + // Get canonical path of workingDirectory + executionCommand.workingDirectory = TermuxFileUtils.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. + // 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); + PluginUtils.processPluginExecutionCommandError(context, this, intent, LOG_TAG, executionCommand, TaskerPlugin.Setting.RESULT_CODE_FAILED); return; } } - Uri scriptUri = new Uri.Builder().scheme("com.termux.file").path(executable).build(); - // Parse arguments_string into a list of arguments like normally done on shells like bourne shell - // Arguemnts are split on whitespaces unless quoted with single or double quotes - // Double quotes and backslashes can be escaped with backslashes in arguments surrounded with double quotes - List arguments_list = ArgumentTokenizer.tokenize(arguments_string); - - Logger.logDebug(context, "execution intent:\n" + - "Executable: `" + executable + "`\n" + - "Arguments:" + getArgumentsString(arguments_list) + "\n" + - "Working Directory: `" + workingDirectory + "`\n" + - "inTerminal: `" + inTerminal + "`"); - - // Create execution intent with the action TermuxService#ACTION_EXECUTE to be sent to the TERMUX_SERVICE - Intent executionIntent = new Intent(ACTION_EXECUTE, scriptUri); - executionIntent.setClassName(Constants.TERMUX_PACKAGE, Constants.TERMUX_SERVICE); - executionIntent.putExtra(PluginBundleManager.EXTRA_ARGUMENTS, arguments_list.toArray(new String[arguments_list.size()])); - if (workingDirectory != null && !workingDirectory.isEmpty()) executionIntent.putExtra(EXTRA_CURRENT_WORKING_DIRECTORY, workingDirectory); - if (!inTerminal) executionIntent.putExtra(EXTRA_EXECUTE_IN_BACKGROUND, true); + // 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; + } - // Send execution intent to TERMUX_SERVICE - PluginResultsService.sendExecuteIntentToExecuteService(context, this, intent, executionIntent, !inTerminal); - } + executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(executionCommand.executable).build(); - private String getArgumentsString(List arguments_list) { - if (arguments_list==null || arguments_list.size() == 0) return ""; - StringBuilder arguments_list_string = new StringBuilder("\n```\n"); - for(int i = 0; i != arguments_list.size(); i++) { - arguments_list_string.append("Arg ").append(i).append(": `").append(arguments_list.get(i)).append("`\n"); - } - arguments_list_string.append("```"); + // Parse arguments_string into a list of arguments like normally done on shells like bourne shell + // Arguments are split on whitespaces unless quoted with single or double quotes + // Double quotes and backslashes can be escaped with backslashes in arguments surrounded + // with double quotes + List arguments_list = new ArrayList<>(); + if (!DataUtils.isNullOrEmpty(arguments_string)) + arguments_list = ArgumentTokenizer.tokenize(arguments_string); + executionCommand.arguments = arguments_list.toArray(new String[0]); + + + Logger.logVerboseExtended(LOG_TAG, executionCommand.toString()); + Logger.logVerbose(LOG_TAG, "Wait For Result: `" + waitForResult + "`"); + + // Create execution intent with the action TERMUX_SERVICE#ACTION_SERVICE_EXECUTE to be sentto the TERMUX_SERVICE + Intent executionIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, executionCommand.executableUri); + executionIntent.setClassName(TermuxConstants.TERMUX_PACKAGE_NAME, TermuxConstants.TERMUX_APP.TERMUX_SERVICE_NAME); + + executionIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, executionCommand.arguments); + if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) + executionIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, executionCommand.workingDirectory); + executionIntent.putExtra(TERMUX_SERVICE.EXTRA_STDIN, executionCommand.stdin); + executionIntent.putExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION, executionCommand.sessionAction); + executionIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL, DataUtils.getStringFromInteger(executionCommand.backgroundCustomLogLevel, null)); + executionIntent.putExtra(TERMUX_SERVICE.EXTRA_RUNNER, executionCommand.runner); // Runner extra will be prioritized over background extra. + executionIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, ExecutionCommand.Runner.APP_SHELL.getName().equals(executionCommand.runner)); // Backward compatibility for runner. + executionIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP, context.getString(R.string.plugin_api_help, TermuxConstants.TERMUX_TASKER_GITHUB_REPO_URL)); - return arguments_list_string.toString(); + // Send execution intent to TERMUX_SERVICE + PluginUtils.sendExecuteIntentToExecuteService(context, this, intent, executionIntent, waitForResult); } } diff --git a/app/src/main/java/com/termux/tasker/PluginBundleManager.java b/app/src/main/java/com/termux/tasker/PluginBundleManager.java index 3f72bb1..6be4103 100644 --- a/app/src/main/java/com/termux/tasker/PluginBundleManager.java +++ b/app/src/main/java/com/termux/tasker/PluginBundleManager.java @@ -1,66 +1,80 @@ package com.termux.tasker; +import android.annotation.SuppressLint; import android.content.Context; import android.os.Bundle; +import android.os.Parcel; import android.text.TextUtils; -import com.termux.tasker.utils.PluginUtils; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.termux.shared.android.PackageUtils; +import com.termux.shared.data.DataUtils; +import com.termux.shared.logger.Logger; +import com.termux.shared.termux.TermuxConstants; /** * Class for managing the {@link com.twofortyfouram.locale.Intent#EXTRA_BUNDLE} for this plug-in. */ -final class PluginBundleManager { +public class PluginBundleManager { - /** - * Type: {@code String}. - * - * The path to the executable to execute. - */ - public static final String EXTRA_EXECUTABLE = "com.termux.tasker.extra.EXECUTABLE"; + /** The {@code String} extra for the path to the executable to execute. */ + public static final String EXTRA_EXECUTABLE = TermuxConstants.TERMUX_TASKER_PACKAGE_NAME + ".extra.EXECUTABLE"; // Default: "com.termux.tasker.extra.EXECUTABLE" - /** - * Type: {@code sting}. - * - * The arguments to pass to the script. - */ - public static final String EXTRA_ARGUMENTS = "com.termux.execute.arguments"; + /** The {@code String} extra for the arguments to pass to the executable. */ + public static final String EXTRA_ARGUMENTS = TermuxConstants.TERMUX_PACKAGE_NAME + ".execute.arguments"; // Default: "com.termux.execute.arguments" - /** - * Type: {@code String}. - * - * The path to current working directory for execution. + /** The {@code String} extra for path to current working directory for execution. */ + public static final String EXTRA_WORKDIR = TermuxConstants.TERMUX_TASKER_PACKAGE_NAME + ".extra.WORKDIR"; // Default: "com.termux.tasker.extra.WORKDIR" + + /** The {@code String} extra for stdin for background commands. */ + public static final String EXTRA_STDIN = TermuxConstants.TERMUX_TASKER_PACKAGE_NAME + ".extra.STDIN"; // Default: "com.termux.tasker.extra.STDIN" + + /** The {@code String} extra for terminal session action defined by + * {@link com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE} + * `VALUE_EXTRA_SESSION_ACTION_*` values. */ - public static final String EXTRA_WORKDIR = "com.termux.tasker.extra.WORKDIR"; + public static final String EXTRA_SESSION_ACTION = TermuxConstants.TERMUX_TASKER_PACKAGE_NAME + ".extra.SESSION_ACTION"; // Default: "com.termux.tasker.extra.SESSION_ACTION" - /** - * Type: {@code boolean}. - * - * If the executable should be run inside a terminal. + /** The {@code String} extra for custom log levels for background commands between + * {@link Logger#LOG_LEVEL_OFF} and {@link Logger#MAX_LOG_LEVEL} as per + * https://github.com/termux/termux-app/commit/60f37bde. */ - public static final String EXTRA_TERMINAL = "com.termux.tasker.extra.TERMINAL"; + public static final String EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL = TermuxConstants.TERMUX_TASKER_PACKAGE_NAME + ".extra.BACKGROUND_CUSTOM_LOG_LEVEL"; // Default: "com.termux.tasker.extra.BACKGROUND_CUSTOM_LOG_LEVEL" + + /** The {@code boolean} extra for whether the executable should be run inside a terminal. */ + public static final String EXTRA_TERMINAL = TermuxConstants.TERMUX_TASKER_PACKAGE_NAME + ".extra.TERMINAL"; // Default: "com.termux.tasker.extra.TERMINAL" + + /** The {@code boolean} extra for whether plugin action should wait for result of commands or not. */ + public static final String EXTRA_WAIT_FOR_RESULT = TermuxConstants.TERMUX_TASKER_PACKAGE_NAME + ".extra.WAIT_FOR_RESULT"; // Default: "com.termux.tasker.extra.WAIT_FOR_RESULT" + + /** - * Type: {@code int}. - *

- * versionCode of the plug-in that saved the Bundle. + * The {@code int} extra for the versionCode of the plugin app that saved the Bundle. * * This extra is not strictly required, however it makes backward and forward compatibility significantly * easier. For example, suppose a bug is found in how some version of the plug-in stored its Bundle. By * having the version, the plug-in can better detect when such bugs occur. */ - public static final String BUNDLE_EXTRA_INT_VERSION_CODE = "com.termux.tasker.extra.VERSION_CODE"; + public static final String BUNDLE_EXTRA_INT_VERSION_CODE = TermuxConstants.TERMUX_TASKER_PACKAGE_NAME + ".extra.VERSION_CODE"; // Default: "com.termux.tasker.extra.VERSION_CODE" + + public static final String UNICODE_CHECK = "\u2713"; + public static final String UNICODE_UNCHECK = "\u2715"; /** * Method to verify the content of the bundle are correct. *

* This method will not mutate {@code bundle}. * - * @param context to get error string. - * @param bundle bundle to verify. May be null, which will always return false. - * @return errmsg if Bundle is not valid, otherwise null. + * @param context The {@link Context} to get error string. + * @param bundle The {@link Bundle} to verify. May be {@code null}, which will always return {@code false}. + * @return Returns the {@code errmsg} if Bundle is not valid, otherwise {@code null}. */ - public static String isBundleValid(final Context context, final Bundle bundle) { - if (bundle == null) return context.getString(R.string.null_or_empty_executable); + @SuppressLint("DefaultLocale") + public static String parseBundle(@NonNull final Context context, final Bundle bundle) { + if (bundle == null) return context.getString(R.string.error_null_bundle); /* * Make sure the correct number of extras exist. @@ -70,7 +84,11 @@ public static String isBundleValid(final Context context, final Bundle bundle) { * - BUNDLE_EXTRA_INT_VERSION_CODE * The bundle may optionally contain: * - EXTRA_WORKDIR + * - EXTRA_STDIN + * - EXTRA_SESSION_ACTION + * - EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL * - EXTRA_TERMINAL + * - EXTRA_WAIT_FOR_RESULT * - VARIABLE_REPLACE_KEYS */ @@ -87,13 +105,13 @@ public static String isBundleValid(final Context context, final Bundle bundle) { } /* - * Check if bundle contains at least 3 keys but no more than 6. + * Check if bundle contains at least 3 keys but no more than 10. * Run this test after checking for required Bundle extras above so that the error message * is more useful. (E.g. the caller will see what extras are missing, rather than just a * message that there is the wrong number). */ - if (bundle.keySet().size() < 3 || bundle.keySet().size() > 6) { - return String.format("The bundle must contain 3-6 keys, but currently contains %d keys.", bundle.keySet().size()); + if (bundle.keySet().size() < 3 || bundle.keySet().size() > 10) { + return String.format("The bundle must contain 3-10 keys, but currently contains %d keys.", bundle.keySet().size()); } if (TextUtils.isEmpty(bundle.getString(EXTRA_EXECUTABLE))) { @@ -104,16 +122,83 @@ public static String isBundleValid(final Context context, final Bundle bundle) { return String.format("The bundle extra %s appears to be the wrong type. It must be an int.", BUNDLE_EXTRA_INT_VERSION_CODE); } + if (!bundle.containsKey(EXTRA_WAIT_FOR_RESULT)) { + // Termux:Tasker <= v0.5 did not have the EXTRA_WAIT_FOR_RESULT key so we only wait + // for results for background commands + if (bundle.containsKey(EXTRA_TERMINAL)) + bundle.putBoolean(EXTRA_WAIT_FOR_RESULT, !bundle.getBoolean(EXTRA_TERMINAL)); + else + bundle.putBoolean(EXTRA_WAIT_FOR_RESULT, true); + } + return null; } - public static Bundle generateBundle(final Context context, final String executable, final String arguments, final String workingDirectory, final boolean inTerminal) { + @Nullable + public static Bundle generateBundle(@NonNull final Context context, final String executable, + final String arguments, final String workingDirectory, + final String stdin, final String sessionAction, + final String backgroundCustomLogLevel, + final boolean inTerminal, final boolean waitForResult) { final Bundle result = new Bundle(); result.putString(EXTRA_EXECUTABLE, executable); result.putString(EXTRA_ARGUMENTS,arguments); result.putString(EXTRA_WORKDIR, workingDirectory); result.putBoolean(EXTRA_TERMINAL, inTerminal); - result.putInt(BUNDLE_EXTRA_INT_VERSION_CODE, PluginUtils.getVersionCode(context)); + result.putBoolean(EXTRA_WAIT_FOR_RESULT, waitForResult); + + result.putString(EXTRA_STDIN, stdin); + result.putString(EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL, backgroundCustomLogLevel); + result.putString(EXTRA_SESSION_ACTION, sessionAction); + + Integer versionCode = PackageUtils.getVersionCodeForPackage(context); + if (versionCode == null) { + Logger.showToast(context, context.getString(R.string.error_get_version_code_failed, context.getPackageName()), true); + return null; + } + + result.putInt(BUNDLE_EXTRA_INT_VERSION_CODE, versionCode); return result; } + + /** + * The message that will be displayed by the plugin host app for the action configuration. + * Blurb length can be a maximum of 60 characters as defined by locale lib. + * @return A blurb for the plug-in. + */ + public static String generateBlurb(@NonNull final Context context, final String executable, + final String arguments, final String workingDirectory, + final String stdin, final String sessionAction, + final String backgroundCustomLogLevel, + final boolean inTerminal, final boolean waitForResult) { + StringBuilder builder = new StringBuilder(); + builder.append(context.getString(R.string.blurb_executable_and_arguments, executable, + arguments == null ? "" : " " + (arguments.length() > 20 ? arguments.substring(0, 20) : arguments))); + builder.append("\n\n").append(context.getString(R.string.blurb_working_directory, (!DataUtils.isNullOrEmpty(workingDirectory) ? UNICODE_CHECK : UNICODE_UNCHECK))); + + if (!inTerminal) { + builder.append("\n").append(context.getString(R.string.blurb_stdin, (!DataUtils.isNullOrEmpty(stdin) ? UNICODE_CHECK : UNICODE_UNCHECK))); + builder.append("\n").append(context.getString(R.string.blurb_custom_log_level, backgroundCustomLogLevel)); + } else { + if (!DataUtils.isNullOrEmpty(sessionAction)) + builder.append("\n").append(context.getString(R.string.blurb_session_action, sessionAction)); + } + + builder.append("\n").append(context.getString(R.string.blurb_in_terminal, (inTerminal ? UNICODE_CHECK : UNICODE_UNCHECK))); + builder.append("\n").append(context.getString(R.string.blurb_wait_for_result, (waitForResult ? UNICODE_CHECK : UNICODE_UNCHECK))); + + String blurb = builder.toString(); + final int maxBlurbLength = 120; // R.integer.twofortyfouram_locale_maximum_blurb_length is set to 60 but we are ignoring that since Tasker doesn't have that limit. + return (blurb.length() > maxBlurbLength) ? blurb.substring(0, maxBlurbLength) : blurb; + } + + /** Get size of {@link Bundle} when stored as a {@link Parcel}. */ + public static int getBundleSize(@NonNull Bundle bundle) { + Parcel parcel = Parcel.obtain(); + bundle.writeToParcel(parcel, 0); + int size = parcel.dataSize(); + parcel.recycle(); + return size; + } + } diff --git a/app/src/main/java/com/termux/tasker/PluginResultsService.java b/app/src/main/java/com/termux/tasker/PluginResultsService.java index a83098c..087261c 100644 --- a/app/src/main/java/com/termux/tasker/PluginResultsService.java +++ b/app/src/main/java/com/termux/tasker/PluginResultsService.java @@ -1,108 +1,18 @@ package com.termux.tasker; -import android.app.Activity; import android.app.IntentService; -import android.app.PendingIntent; -import android.content.BroadcastReceiver; -import android.content.Context; import android.content.Intent; -import android.os.Build; -import android.os.Bundle; import androidx.annotation.Nullable; -import com.termux.tasker.utils.Logger; - -import java.util.regex.Pattern; - -/** - * A unified class and service to handle sending of plugin commands to the execution service, - * receiving the results of plugin commands back from the execution service and sending immediate or - * pending results back to the plugin host. - * - * This is currently designed and tested with Tasker as the plugin host app and the - * com.termux.app/.TermuxService as the execution service of plugin commands but should work with - * other plugin host apps. The TermuxService will run the commands with the BackgroundJob class if - * background mode is enabled and results of commands are only be returned in this case. If - * background mode is not enabled, a foreground terminal session will be opened and results are not - * returned. - * - * Flow for the usage of {@link PluginResultsService}: - * 1. Call {@link #sendExecuteIntentToExecuteService} from {@link FireReceiver}. This expects the - * original intent received by {@link FireReceiver}, the execution intent that should be started to - * call the execution service containing its ComponentName and any extras required to run the - * plugin commands, and a boolean for whether commands are to be run in background mode or not. - * If the plugin action has a timeout greater than 0 and background mode is enabled, then the - * function will automatically create a {@link android.app.PendingIntent} that can be used to - * return the results back to {@link PluginResultsService} and add the original - * {@link android.content.Intent} received by {@link FireReceiver} as {@link #EXTRA_ORIGINAL_INTENT} - * to it and then add the created {@link android.app.PendingIntent} to the execution intent as - * {@link #EXTRA_PENDING_INTENT} extra. - * Otherwise, the function immediaterly returns - * {@link com.termux.tasker.TaskerPlugin.Setting#RESULT_CODE_OK} back to the plugin host app using - * the {@link #sendImmediateResultToPluginHostApp} function and the flow ends here for the usage - * of {@link PluginResultsService}. - * - * 2. The execution service should receive the execution intent and run the required plugin commands. - * If background mode is enabled, then results should be returned back to the - * {@link PluginResultsService}. For this, optionally call - * {@link #sendExecuteResultToResultsService} or create the result intent manually and send it back - * using the {@link android.app.PendingIntent} received by the {@link PluginResultsService}. The - * pending intent should already contain the original {@link android.content.Intent} received by the - * {@link FireReceiver}. A result {@link Bundle} object with the {@link #EXTRA_RESULT_BUNDLE} - * key should also be sent back in an {@link android.content.Intent} using the - * {@link android.app.PendingIntent#send(Context, int, Intent)} function. The bundle can contain - * the keys {@link #EXTRA_STDOUT} (String), {@link #EXTRA_STDERR} (String), - * {@link #EXTRA_EXIT_CODE} (Integer), {@link #PLUGIN_VARIABLE_ERR} (Integer) and - * {@link #PLUGIN_VARIABLE_ERRMSG} (String) who values will be sent back to the plugin host app. - * - * 3. The {@link android.app.PendingIntent} sent is received by the {@link PluginResultsService} - * with the {@link #onHandleIntent} function which calls the - * {@link #sendPendingResultToPluginHostApp} function with the intent received. - * - * 4. The {@link #sendPendingResultToPluginHostApp} function extracts the original intent and the - * result bundle from the intent received and calls the {@link #createVariablesBundle} function - * to create the variables bundle to be sent back to plugin host and then sends it with - * {@link com.termux.tasker.TaskerPlugin.Setting#signalFinish} function. If any of the Integer keys - * do not exist in the result bundle or if the values of String keys are null or empty in the result - * bundle, then they are not sent back to the plugin host. The flow ends here. - * - * The {@link #sendImmediateResultToPluginHostApp} function can be used to send result back to the - * plugin host immediately like in case there is an error processing the plugin command request or - * if the result should not be expected to be sent back by the execution service. - * - * The {@link #createVariablesBundle} function creates a variables bundle that can be sent back to - * the plugin host. The bundle will contain the keys {@link #PLUGIN_VARIABLE_STDOUT} (String), - * {@link #PLUGIN_VARIABLE_STDERR} (String), {@link #PLUGIN_VARIABLE_EXIT_CODE} (String) and - * {@link #PLUGIN_VARIABLE_ERRMSG} (String). The {@link #PLUGIN_VARIABLE_ERRMSG} key will only be - * added if the {@link #PLUGIN_VARIABLE_ERR} value to be sent back to the plugin host is greater - * than {@link com.termux.tasker.TaskerPlugin.Setting#RESULT_CODE_OK}. Any null or empty values are - * not added to the variables bundle. - * - * The value for {@link #PLUGIN_VARIABLE_ERR} is first sanitized by the {@link #sanitizeErrCode} - * function before it is sent back to the plugin host. - */ +import com.termux.shared.logger.Logger; +import com.termux.tasker.utils.PluginUtils; public class PluginResultsService extends IntentService { - public static final String PLUGIN_VARIABLE_STDOUT = TaskerPlugin.VARIABLE_PREFIX + "stdout"; //plugin variable for stdout value of termux command - public static final String PLUGIN_VARIABLE_STDERR = TaskerPlugin.VARIABLE_PREFIX + "stderr"; //plugin variable for stderr value of termux command - public static final String PLUGIN_VARIABLE_EXIT_CODE = TaskerPlugin.VARIABLE_PREFIX + "result"; //plugin variable for exit code of termux command - public static final String PLUGIN_VARIABLE_ERR = TaskerPlugin.VARIABLE_PREFIX + "err"; //plugin variable for err value of plugin action - public static final String PLUGIN_VARIABLE_ERRMSG = TaskerPlugin.Setting.VARNAME_ERROR_MESSAGE; //plugin variable for errmsg value of plugin action - - public static final String EXTRA_STDOUT = "stdout"; //string extra for stdout value of termux command - public static final String EXTRA_STDERR = "stderr"; //string extra for stderr value of termux command - public static final String EXTRA_EXIT_CODE = "exitCode"; //int extra for exit code value of termux command - public static final String EXTRA_ERR = "err"; //int extra for err value of plugin action - public static final String EXTRA_ERRMSG = "errmsg"; //string extra for errmsg value of plugin action - - public static final String EXTRA_RESULT_BUNDLE = "result"; //bundle extra containing result of commands to send back to plugin host app - public static final String EXTRA_ORIGINAL_INTENT = "originalIntent"; //parcelable extra containing original intent received from plugin host app by FireReceiver + public static final String PLUGIN_SERVICE_LABEL = "PluginResultsService"; - public static final String EXTRA_PENDING_INTENT = "pendingIntent"; //parcelable extra for execution intent containing pending intent for the PluginResultsService and the original intent received by FireReceiver + private static final String LOG_TAG = "PluginResultsService"; - public static final String PLUGIN_SERVICE_LABEL = "PluginResultsService"; - public static final String EXECUTION_SERVICE_LABEL = "TermuxService"; public PluginResultsService(){ super(PLUGIN_SERVICE_LABEL); @@ -111,288 +21,15 @@ public PluginResultsService(){ /** * Receive intent containing result of commands and send pending result back to plugin host app. * - * @param intent containing result and original intent received by {@link FireReceiver}. + * @param intent The {@link Intent} containing result and original intent received by {@link FireReceiver}. */ @Override protected void onHandleIntent(@Nullable Intent intent) { - Logger.logInfo(this,PLUGIN_SERVICE_LABEL + " received execution result from " + EXECUTION_SERVICE_LABEL); - sendPendingResultToPluginHostApp(this, intent); - } - - /** - * Send execution intent to execution service containing command information and original intent - * received by {@link FireReceiver}. - * - * @param context that will be used to send execution intent to the execution service. - * @param receiver of the originalIntent broadcast. - * @param originalIntent received by {@link FireReceiver}. - * @param executionIntent to be sent to execution service containing command information. - */ - public static void sendExecuteIntentToExecuteService(final Context context, final BroadcastReceiver receiver, final Intent originalIntent, final Intent executionIntent, final boolean executeInBackground) { - if (context == null) return; - - if (executionIntent == null) { - Logger.logError(context, "The executionIntent passed to sendExecuteIntentToExecuteService() cannot be null."); - return; - } - - if (executionIntent.getComponent() == null) { - Logger.logError(context, "The Component for the executionIntent passed to sendExecuteIntentToExecuteService() cannot be null."); - return; - } - - Logger.logDebug(context,"Sending execution intent to " + EXECUTION_SERVICE_LABEL); - - // If timeout for plugin action is greater than 0 and execute in background is enabled - if (receiver != null && receiver.isOrderedBroadcast() && executeInBackground) { - // Notify plugin host app that result will be sent later - // Result should be sent to PluginResultsService via a PendingIntent by execution service after commands have finished executing - receiver.setResultCode(TaskerPlugin.Setting.RESULT_CODE_PENDING); - - // Create intent for PluginResultsService class and add original intent received by FireReceiver to it - Intent pluginResultsServiceIntent = new Intent(context, PluginResultsService.class); - pluginResultsServiceIntent.putExtra(PluginResultsService.EXTRA_ORIGINAL_INTENT, originalIntent); - - // Create PendingIntent that can be used by execution service to send result of commands back to PluginResultsService - PendingIntent pendingIntent = PendingIntent.getService(context, 1, pluginResultsServiceIntent, PendingIntent.FLAG_ONE_SHOT); - executionIntent.putExtra(EXTRA_PENDING_INTENT, pendingIntent); - } else { - // If execute in background is not enabled, do not expect results back from execution service and return result now so that plugin action does not timeout - sendImmediateResultToPluginHostApp(context, receiver, originalIntent, null, null, null, TaskerPlugin.Setting.RESULT_CODE_OK, null); - } - - // Send execution intent to execution service - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // https://developer.android.com/about/versions/oreo/background.html - context.startForegroundService(executionIntent); - } else { - context.startService(executionIntent); - } - } - - /** - * Send execution result of commands to PluginResultsService with the PendingIntent received by - * execution service from {@link FireReceiver}. - * - * @param context that will be used to send result intent to the PluginResultsService. - * @param pendingIntent sent by {@link FireReceiver} to the execution service. - * @param stdout value for {@link #EXTRA_STDOUT} extra of {@link #EXTRA_RESULT_BUNDLE} bundle of intent. - * @param stderr value for {@link #EXTRA_STDERR} extra of {@link #EXTRA_RESULT_BUNDLE} bundle of intent. - * @param exitCode value for {@link #EXTRA_EXIT_CODE} extra of {@link #EXTRA_RESULT_BUNDLE} bundle of intent. - * @param errCode value for {@link #EXTRA_ERR} extra of {@link #EXTRA_RESULT_BUNDLE} bundle of intent. - * @param errmsg value for {@link #EXTRA_ERRMSG} extra of {@link #EXTRA_RESULT_BUNDLE} bundle of intent. - */ - public static void sendExecuteResultToResultsService(final Context context, final PendingIntent pendingIntent, final String stdout, final String stderr, final String exitCode, final String errCode, final String errmsg) { - - Logger.logDebug(context, "Sending execution result to " + PLUGIN_SERVICE_LABEL + ":\n" + - EXTRA_STDOUT + ": `" + stdout + "`\n" + - EXTRA_STDERR + ": `" + stderr + "`\n" + - EXTRA_EXIT_CODE + ": `" + exitCode + "`\n" + - EXTRA_ERR + ": `" + errCode + "`\n" + - EXTRA_ERRMSG + ": `" + errmsg + "`"); - - final Bundle resultBundle = new Bundle(); - - resultBundle.putString(EXTRA_STDOUT, stdout); - resultBundle.putString(EXTRA_STDERR, stderr); - if (exitCode != null && !exitCode.isEmpty()) resultBundle.putInt(EXTRA_EXIT_CODE, Integer.parseInt(exitCode)); - if (errCode != null && !errCode.isEmpty()) resultBundle.putInt(EXTRA_ERR, Integer.parseInt(errCode)); - resultBundle.putString(EXTRA_ERRMSG, errmsg); - - Intent resultIntent = new Intent(); - resultIntent.putExtra(EXTRA_RESULT_BUNDLE, resultBundle); - - if(pendingIntent != null && context != null) { - try { - pendingIntent.send(context, Activity.RESULT_OK, resultIntent); - } catch (PendingIntent.CanceledException e) { - // The caller doesn't want the result? That's fine, just ignore - } - } - } - - /** - * Send immediate result to plugin host app in a variables bundle. - * - * @param context for logging. - * @param receiver of the originalIntent broadcast. - * @param originalIntent received by {@link FireReceiver}. - * @param stdout value for {@link #PLUGIN_VARIABLE_STDOUT} variable of plugin action. - * @param stderr value for {@link #PLUGIN_VARIABLE_STDERR} variable of plugin action. - * @param exitCode value for {@link #PLUGIN_VARIABLE_EXIT_CODE} variable of plugin action. - * @param errCode value for {@link #PLUGIN_VARIABLE_ERR} variable of plugin action. - * @param errmsg value for {@link #PLUGIN_VARIABLE_ERRMSG} variable of plugin action. - */ - public static void sendImmediateResultToPluginHostApp(final Context context, final BroadcastReceiver receiver, final Intent originalIntent, final String stdout, final String stderr, final String exitCode, final int errCode, final String errmsg) { - if (receiver == null) return; - - // If timeout for plugin action is 0, then don't send anything - if (!receiver.isOrderedBroadcast()) return; - - int err = sanitizeErrCode(context, errCode); - - Logger.logInfo(context,"Sending immediate result to plugin host app. " + PLUGIN_VARIABLE_ERR + ": " + ((err == TaskerPlugin.Setting.RESULT_CODE_OK) ? "success" : "failed") + " (" + err + ")"); - - if (TaskerPlugin.Setting.hostSupportsVariableReturn(originalIntent.getExtras())) { - final Bundle varsBundle = createVariablesBundle(context, stdout, stderr, exitCode, err, errmsg); - TaskerPlugin.addVariableBundle(receiver.getResultExtras(true), varsBundle); - } - - receiver.setResultCode(err); - } - - /** - * Send pending result to plugin host app in a variables bundle. - * - * @param context that will be used to send variables bundle to plugin host app and logging. - * @param intent containing result and original intent received by {@link FireReceiver}. - */ - public static void sendPendingResultToPluginHostApp(final Context context, final Intent intent) { - if (intent == null){ - Logger.logWarn(context, "Ignoring null intent passed to sendPendingResultToPluginHostApp()."); - return; - } - - final Intent originalIntent = intent.getParcelableExtra(EXTRA_ORIGINAL_INTENT); - if (originalIntent == null) { - Logger.logError(context, "The intent passed to sendPendingResultToPluginHostApp() must contain the original intent received by the FireReceiver at the " + EXTRA_ORIGINAL_INTENT + " key."); - return; - } - - final Bundle resultBundle = intent.getBundleExtra(EXTRA_RESULT_BUNDLE); - if (resultBundle == null) { - Logger.logError(context, "The intent passed to sendPendingResultToPluginHostApp() must contain the result bundle at the " + EXTRA_RESULT_BUNDLE + " key."); - return; - } - - int err = TaskerPlugin.Setting.RESULT_CODE_OK; - // This check is necessary, otherwise default value will be 0 if extra does not exist, - // and so plugin host app like Tasker will consider the action as failed for the value 0, - // since it equals Activity.RESULT_CANCELED (0) instead of TaskerPlugin.Setting.RESULT_CODE_OK/Activity.RESULT_OK (-1) - if (resultBundle.containsKey(EXTRA_ERR)) - err = sanitizeErrCode(context, resultBundle.getInt(EXTRA_ERR)); - - Logger.logInfo(context, "Sending pending result to plugin host app. " + PLUGIN_VARIABLE_ERR + ": " + ((err == TaskerPlugin.Setting.RESULT_CODE_OK) ? "success" : "failed") + " (" + err + ")"); - - String exitCode = null; - if (resultBundle.containsKey(EXTRA_EXIT_CODE)) - exitCode = Integer.toString(resultBundle.getInt(EXTRA_EXIT_CODE)); - - final Bundle varsBundle = createVariablesBundle(context, resultBundle.getString(EXTRA_STDOUT, ""), resultBundle.getString(EXTRA_STDERR, ""), exitCode, err, resultBundle.getString(EXTRA_ERRMSG, "")); - - if(context != null) - TaskerPlugin.Setting.signalFinish(context, originalIntent, err, varsBundle); - } - - /** - * Create variables bundle to send back to plugin host app. - * - * @param context for logging. - * @param stdout value for {@link #PLUGIN_VARIABLE_STDOUT} variable of plugin action. - * @param stderr value for {@link #PLUGIN_VARIABLE_STDERR} variable of plugin action. - * @param exitCode value for {@link #PLUGIN_VARIABLE_EXIT_CODE} variable of plugin action. - * @param errCode value for {@link #PLUGIN_VARIABLE_ERR} variable of plugin action. - * @param errmsg value for {@link #PLUGIN_VARIABLE_ERRMSG} variable of plugin action. - * @return variables bundle. - */ - public static Bundle createVariablesBundle(final Context context, String stdout, String stderr, String exitCode, int errCode, String errmsg) { - - Logger.logDebug(context, "Variables bundle for plugin host app:\n" + - PLUGIN_VARIABLE_STDOUT + ": `" + stdout + "`\n" + - PLUGIN_VARIABLE_STDERR + ": `" + stderr + "`\n" + - PLUGIN_VARIABLE_EXIT_CODE + ": `" + exitCode + "`\n" + - PLUGIN_VARIABLE_ERR + ": `" + errCode + "`\n" + - PLUGIN_VARIABLE_ERRMSG + ": `" + errmsg + "`"); - - if (errCode == TaskerPlugin.Setting.RESULT_CODE_OK && errmsg != null && !errmsg.isEmpty()) { - Logger.logWarn(context, "Ignoring setting " + PLUGIN_VARIABLE_ERRMSG + " variable since " + PLUGIN_VARIABLE_ERR + " is set to RESULT_CODE_OK \"" + TaskerPlugin.Setting.RESULT_CODE_OK + "\", " + PLUGIN_VARIABLE_ERRMSG + ": \"" + errmsg + "\""); - errmsg = ""; - } - - // Send back empty values for variables not to be returned. - // This will/should unset their respective variables in the plugin host app, - // since if multiple actions are run in the same task, some variables from previous actions - // may still be set and get mixed in with current ones. - if (stdout == null) stdout = ""; - if (stderr == null) stderr = ""; - if (exitCode == null) exitCode = ""; - if (errmsg == null) errmsg = ""; - - final Bundle variablesBundle = new Bundle(); - - if (isPluginHostAppVariableNameValid(context, PLUGIN_VARIABLE_STDOUT)) - variablesBundle.putString(PLUGIN_VARIABLE_STDOUT, stdout); - if (isPluginHostAppVariableNameValid(context, PLUGIN_VARIABLE_STDERR)) - variablesBundle.putString(PLUGIN_VARIABLE_STDERR, stderr); - if (isPluginHostAppVariableNameValid(context, PLUGIN_VARIABLE_EXIT_CODE)) - variablesBundle.putString(PLUGIN_VARIABLE_EXIT_CODE, exitCode); - if (isPluginHostAppVariableNameValid(context, PLUGIN_VARIABLE_ERRMSG)) - variablesBundle.putString(PLUGIN_VARIABLE_ERRMSG, errmsg); - - return variablesBundle; - } - - /** - * Sanitize errCode value so that it can be sent back to plugin host app as %err value. - * For custom result codes for the plugin, start numbering from - * {@link com.termux.tasker.TaskerPlugin.Setting#RESULT_CODE_FAILED_PLUGIN_FIRST}, - * otherwise plugin host app like Tasker will consider them as unknown result codes. - * - * @param context for logging. - * @param errCode value to sanitize. - * @return errCode value as is if valid, otherwise returns - * {@link com.termux.tasker.TaskerPlugin.Setting#RESULT_CODE_OK}. - */ - public static int sanitizeErrCode(final Context context, final int errCode) { - int err; - if (errCode >= TaskerPlugin.Setting.RESULT_CODE_OK) { - err = errCode; - } else { - Logger.logWarn(context, "Ignoring invalid " + PLUGIN_VARIABLE_ERR + " value \"" + errCode + "\" for plugin action and force setting it to RESULT_CODE_OK \"" + TaskerPlugin.Setting.RESULT_CODE_OK + "\""); - err = TaskerPlugin.Setting.RESULT_CODE_OK; - } - - return err; - } - - /** - * Checks if plugin host variable name is valid and can be sent back to plugin host app. - * - * @param context for logging. - * @param name of plugin variable. - * @return true if valid, otherwise false. - */ - public static boolean isPluginHostAppVariableNameValid(final Context context, final String name) { - if (!TaskerPlugin.variableNameValid(name)) { - Logger.logWarn(context, "Ignoring invalid plugin variable name: \"" + name + "\""); - return false; + if (intent != null) { + if(intent.getComponent() != null) + Logger.logInfo(LOG_TAG, PLUGIN_SERVICE_LABEL + " received execution result"); + PluginUtils.sendPendingResultToPluginHostApp(this, intent); } - - return true; - } - - /** - * Determines whether string matches a plugin host app variable. - * - * @return true if string matches a plugin host app variable, otherwise false. - */ - public static boolean isPluginHostAppVariableString(String string) { - String VARIABLE_NAME_CONTAINING_EXPRESSION = "^" + TaskerPlugin.VARIABLE_NAME_MATCH_EXPRESSION + "$"; - Pattern VARIABLE_NAME_CONTAINING_PATTERN = Pattern.compile(VARIABLE_NAME_CONTAINING_EXPRESSION, 0); - - return VARIABLE_NAME_CONTAINING_PATTERN.matcher(string).matches(); - } - - /** - * Determines whether string contains a plugin host app variable. - * - * @return true if string contains a plugin host app variable, otherwise false. - */ - public static boolean isPluginHostAppVariableContainingString(String string) { - String VARIABLE_NAME_CONTAINING_EXPRESSION = ".*" + TaskerPlugin.VARIABLE_NAME_MATCH_EXPRESSION + ".*"; - Pattern VARIABLE_NAME_CONTAINING_PATTERN = Pattern.compile(VARIABLE_NAME_CONTAINING_EXPRESSION, 0); - - return VARIABLE_NAME_CONTAINING_PATTERN.matcher(string).matches(); } } diff --git a/app/src/main/java/com/termux/tasker/TermuxTaskerApplication.java b/app/src/main/java/com/termux/tasker/TermuxTaskerApplication.java new file mode 100644 index 0000000..4cb4587 --- /dev/null +++ b/app/src/main/java/com/termux/tasker/TermuxTaskerApplication.java @@ -0,0 +1,39 @@ +package com.termux.tasker; + +import android.app.Application; +import android.content.Context; +import android.util.Log; + +import com.termux.shared.logger.Logger; +import com.termux.shared.termux.TermuxConstants; +import com.termux.shared.termux.crash.TermuxCrashUtils; +import com.termux.shared.termux.settings.preferences.TermuxTaskerAppSharedPreferences; + +public class TermuxTaskerApplication extends Application { + + public static final String LOG_TAG = "TermuxTaskerApplication"; + + public void onCreate() { + super.onCreate(); + + Log.i(LOG_TAG, "AppInit"); + + Context context = getApplicationContext(); + + // Set crash handler for the app + TermuxCrashUtils.setCrashHandler(context); + + // Set log config for the app + setLogConfig(context, true); + } + + public static void setLogConfig(Context context, boolean commitToFile) { + Logger.setDefaultLogTag(TermuxConstants.TERMUX_TASKER_APP_NAME.replaceAll("[: ]", "")); + + // Load the log level from shared preferences and set it to the Logger.CURRENT_LOG_LEVEL + TermuxTaskerAppSharedPreferences preferences = TermuxTaskerAppSharedPreferences.build(context); + if (preferences == null) return; + preferences.setLogLevel(null, preferences.getLogLevel(true), commitToFile); + } + +} diff --git a/app/src/main/java/com/termux/tasker/activities/TermuxTaskerMainActivity.java b/app/src/main/java/com/termux/tasker/activities/TermuxTaskerMainActivity.java new file mode 100644 index 0000000..dfe3708 --- /dev/null +++ b/app/src/main/java/com/termux/tasker/activities/TermuxTaskerMainActivity.java @@ -0,0 +1,100 @@ +package com.termux.tasker.activities; + +import android.os.Bundle; +import android.widget.Button; +import android.widget.TextView; + +import androidx.appcompat.app.AppCompatActivity; + +import com.termux.shared.activity.media.AppCompatActivityUtils; +import com.termux.shared.android.PackageUtils; +import com.termux.shared.logger.Logger; +import com.termux.shared.markdown.MarkdownUtils; +import com.termux.shared.termux.TermuxConstants; +import com.termux.shared.termux.theme.TermuxThemeUtils; +import com.termux.shared.theme.NightMode; +import com.termux.tasker.R; +import com.termux.tasker.TermuxTaskerApplication; + +public class TermuxTaskerMainActivity extends AppCompatActivity { + + public static final String LOG_TAG = "TermuxTaskerMainActivity"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_termux_tasker_main); + + // Set NightMode.APP_NIGHT_MODE + TermuxThemeUtils.setAppNightMode(this); + AppCompatActivityUtils.setNightMode(this, NightMode.getAppNightMode().getName(), true); + + AppCompatActivityUtils.setToolbar(this, com.termux.shared.R.id.toolbar); + AppCompatActivityUtils.setToolbarTitle(this, com.termux.shared.R.id.toolbar, TermuxConstants.TERMUX_TASKER_APP_NAME, 0); + + TextView pluginInfo = findViewById(R.id.textview_plugin_info); + pluginInfo.setText(getString(R.string.plugin_info, TermuxConstants.TERMUX_GITHUB_REPO_URL, + TermuxConstants.TERMUX_TASKER_GITHUB_REPO_URL)); + } + + @Override + protected void onResume() { + super.onResume(); + + // Set log level for the app + TermuxTaskerApplication.setLogConfig(this, false); + + Logger.logVerbose(LOG_TAG, "onResume"); + + setChangeLauncherActivityStateViews(); + } + + + + private void setChangeLauncherActivityStateViews() { + String packageName = TermuxConstants.TERMUX_TASKER_PACKAGE_NAME; + String className = TermuxConstants.TERMUX_TASKER_APP.TERMUX_TASKER_LAUNCHER_ACTIVITY_NAME; + + TextView changeLauncherActivityStateTextView = findViewById(R.id.textview_change_launcher_activity_state_details); + changeLauncherActivityStateTextView.setText(MarkdownUtils.getSpannedMarkdownText(this, + getString(R.string.msg_change_launcher_activity_state_info, packageName, getClass().getName()))); + + Button changeLauncherActivityStateButton = findViewById(R.id.button_change_launcher_activity_state); + String stateChangeMessage; + boolean newState; + + Boolean currentlyDisabled = PackageUtils.isComponentDisabled(this, + packageName, className, false); + if (currentlyDisabled == null) { + Logger.logError(LOG_TAG, "Failed to check if \"" + packageName + "/" + className + "\" launcher activity is disabled"); + changeLauncherActivityStateButton.setEnabled(false); + changeLauncherActivityStateButton.setAlpha(.5f); + changeLauncherActivityStateButton.setText(com.termux.shared.R.string.action_disable_launcher_icon); + changeLauncherActivityStateButton.setOnClickListener(null); + return; + } + + changeLauncherActivityStateButton.setEnabled(true); + changeLauncherActivityStateButton.setAlpha(1f); + if (currentlyDisabled) { + changeLauncherActivityStateButton.setText(com.termux.shared.R.string.action_enable_launcher_icon); + stateChangeMessage = getString(com.termux.shared.R.string.msg_enabling_launcher_icon, TermuxConstants.TERMUX_TASKER_APP_NAME); + newState = true; + } else { + changeLauncherActivityStateButton.setText(com.termux.shared.R.string.action_disable_launcher_icon); + stateChangeMessage = getString(com.termux.shared.R.string.msg_disabling_launcher_icon, TermuxConstants.TERMUX_TASKER_APP_NAME); + newState = false; + } + + changeLauncherActivityStateButton.setOnClickListener(v -> { + Logger.logInfo(LOG_TAG, stateChangeMessage); + String errmsg = PackageUtils.setComponentState(this, + packageName, className, newState, stateChangeMessage, true); + if (errmsg == null) + setChangeLauncherActivityStateViews(); + else + Logger.logError(LOG_TAG, errmsg); + }); + } + +} diff --git a/app/src/main/java/com/termux/tasker/utils/FileUtils.java b/app/src/main/java/com/termux/tasker/utils/FileUtils.java deleted file mode 100644 index 7f16445..0000000 --- a/app/src/main/java/com/termux/tasker/utils/FileUtils.java +++ /dev/null @@ -1,230 +0,0 @@ -package com.termux.tasker.utils; - -import android.content.Context; - -import com.termux.tasker.Constants; -import com.termux.tasker.R; - -import java.io.File; -import java.util.regex.Pattern; - -public class FileUtils { - - /** - * Replace "$PREFIX/" or "~/" prefix with termux absolute paths. - * - * @param path to expand. - * @return expand path. - */ - public static String getExpandedTermuxPath(String path) { - if(path != null && !path.isEmpty()) { - path = path.replaceAll("^\\$PREFIX$", Constants.PREFIX_PATH); - path = path.replaceAll("^\\$PREFIX/", Constants.PREFIX_PATH + "/"); - path = path.replaceAll("^~/$", Constants.HOME_PATH); - path = path.replaceAll("^~/", Constants.HOME_PATH + "/"); - } - - return path; - } - - /** - * Replace termux absolute paths with "$PREFIX/" or "~/" prefix. - * - * @param path to unexpand. - * @return unexpand path. - */ - public static String getUnExpandedTermuxPath(String path) { - if(path != null && !path.isEmpty()) { - path = path.replaceAll("^" + Pattern.quote(Constants.PREFIX_PATH) + "/", "\\$PREFIX/"); - path = path.replaceAll("^" + Pattern.quote(Constants.HOME_PATH) + "/", "~/"); - } - - return path; - } - - /** - * First calls {@link #getExpandedTermuxPath(String)} on input path, - * then if its already an absolute path, it is returned as is, otherwise - * {@link Constants#TASKER_PATH} is prefixed to the path. - * - * @param path to convert. - * @return absolute path. - */ - public static String getAbsolutePathForExecutable(String path) { - if (path == null) - path = ""; - else - path = getExpandedTermuxPath(path); - - String absolutePath; - - // If path is already an absolute path - if (path.startsWith("/")) - absolutePath = path; - // Otherwise assume executable refers to path in TASKER_PATH - else - absolutePath = Constants.TASKER_PATH + "/" + path; - - try { - absolutePath = new File(absolutePath).getCanonicalPath(); - } catch(Exception e) { - } - - return absolutePath; - } - - /** - * Determines whether path is in {@link Constants#TASKER_PATH}. - * - * @return true if path in {@link Constants#TASKER_PATH}, otherwise false. - */ - public static boolean isPathInTaskerDir(String path) { - try { - path = new File(path).getCanonicalPath(); - } catch(Exception e) { - return false; - } - - return path.startsWith(Constants.TASKER_PATH + "/"); - } - - /** - * Determines whether path is in {@link Constants#HOME_PATH}. - * - * @return true if path in {@link Constants#HOME_PATH}, otherwise false. - */ - public static boolean isPathInTermuxHome(String path) { - try { - path = new File(path).getCanonicalPath(); - } catch(Exception e) { - return false; - } - - return path.startsWith(Constants.HOME_PATH + "/"); - } - - /** - * Check if path is a readable and executable file. - * - * @param context to get error string. - * @param path to check. - * @param setMissingPermissions a boolean that decides if read and execute flags are - * automatically set if path is a regular file in - * {@link Constants#TASKER_PATH}. - * @param ignoreErrorsForTaskerDir a boolean that decides if read and execute errors - * to be ignored if path is in - * {@link Constants#TASKER_PATH}. - * @return errmsg if path is not a regular file, or cannot be read or executed, otherwise - * null. - */ - public static String checkIfExecutableFileIsReadableAndExecutable(final Context context, final String path, final boolean setMissingPermissions, final boolean ignoreErrorsForTaskerDir) { - if (path == null || path.isEmpty()) return context.getString(R.string.null_or_empty_executable); - - String errmsg = null; - - File file = new File(path); - - boolean isPathInTaskerDir = isPathInTaskerDir(path); - - // If file exits but not a regular file - if (file.exists() && !file.isFile()) { - return context.getString(R.string.non_regular_file_found); - } - - // If path is in TASKER_PATH - if (Constants.TASKER_DIR.isDirectory() && isPathInTaskerDir) { - // If setMissingPermissions is enabled and path is a regular file, set read and execute flags to file - if (setMissingPermissions && file.isFile()) { - if (!file.canRead()) file.setReadable(true); - if (!file.canExecute()) file.setExecutable(true); - } - } - - // If path is not a regular file - if (!file.isFile()) { - errmsg = context.getString(R.string.no_regular_file_found); - } - // If path is not is TASKER_PATH or if read and execute errors must not be ignored for files in TASKER_PATH - else if (!isPathInTaskerDir || !ignoreErrorsForTaskerDir) { - // If file is not readable - if (!file.canRead()) { - errmsg = context.getString(R.string.no_readable_file_found); - } - // If file is not executable - // This check will give "avc: granted { execute }" warnings for target sdk 29 - else if (!file.canExecute()) { - errmsg = context.getString(R.string.no_executable_file_found); - } - } - - return errmsg; - - } - - /** - * Check if path is a readable directory. - * - * @param context to get error string. - * @param path path to check. - * @param createDirectoryIfMissing a boolean that decides if directory - * should automatically be created if - * path is in {@link Constants#HOME_PATH}. - * @param setMissingPermissions a boolean that decides if read flags are - * automatically set if path is a directory in - * {@link Constants#HOME_PATH}. - * @param ignoreErrorsForTermuxHome a boolean that decides if existence and - * readable checks are to be ignored if path is - * in {@link Constants#HOME_PATH}. - * @return errmsg if path is not a directory or cannot be read, otherwise - * null. - */ - public static String checkIfDirectoryIsReadable(final Context context, final String path, final boolean createDirectoryIfMissing, final boolean setMissingPermissions, final boolean ignoreErrorsForTermuxHome) { - if (path == null || path.isEmpty()) return context.getString(R.string.null_or_empty_directory); - - String errmsg = null; - - File file = new File(path); - - // If file exits but not a directory file - if (file.exists() && !file.isDirectory()) { - return context.getString(R.string.non_directory_file_found); - } - - boolean isPathInTermuxHome = isPathInTermuxHome(path); - - // If path is in HOME_PATH - if (Constants.HOME_DIR.isDirectory() && isPathInTermuxHome) { - // If createDirectoryIfMissing is enabled and no file exists at path, create directory - if (createDirectoryIfMissing && !file.exists()) { - try { - // If failed to create directory - if (!file.mkdirs()) { - return context.getString(R.string.directory_creation_failed, path); - } - } catch(Exception e) { - return context.getString(R.string.directory_creation_failed_with_exception, path, e.getMessage()); - } - } - - // If setMissingPermissions is enabled and path is a directory, set read flags to directory - if (setMissingPermissions && file.isDirectory()) { - if (!file.canRead()) file.setReadable(true); - } - } - - // If path is not is HOME_PATH or if existence and read checks must not be ignored for files in HOME_PATH - if (!isPathInTermuxHome || !ignoreErrorsForTermuxHome) { - // If path is not a directory - if (!file.isDirectory()) { - errmsg = context.getString(R.string.no_directory_found); - } - // If directory is not readable - else if (!file.canRead()) { - errmsg = context.getString(R.string.no_readable_directory_found); - } - } - - return errmsg; - - } -} diff --git a/app/src/main/java/com/termux/tasker/utils/Logger.java b/app/src/main/java/com/termux/tasker/utils/Logger.java deleted file mode 100644 index 1a75b8b..0000000 --- a/app/src/main/java/com/termux/tasker/utils/Logger.java +++ /dev/null @@ -1,204 +0,0 @@ -package com.termux.tasker.utils; - -import android.content.Context; -import android.os.Handler; -import android.os.Looper; -import android.util.Log; -import android.widget.Toast; - -import androidx.appcompat.app.AlertDialog; - -import com.termux.tasker.R; - -import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; - -public class Logger { - - public static final String DEFAULT_LOG_TAG = "termux-tasker"; - - public static final int LOG_LEVEL_OFF = 0; // log nothing - public static final int LOG_LEVEL_NORMAL = 1; // start logging error, warn and info messages and stacktraces - public static final int LOG_LEVEL_DEBUG = 2; // start logging debug messages - public static final int LOG_LEVEL_VERBOSE = 3; // start logging verbose messages - - public static final int DEFAULT_LOG_LEVEL = LOG_LEVEL_NORMAL; - - static public void logMesssage(Context context, int logLevel, String tag, String message) { - if (context == null) return; - - int currentLogLevel = getLogLevel(context); - - if(logLevel == Log.ERROR && currentLogLevel >= LOG_LEVEL_NORMAL) - Log.e(tag, message); - else if(logLevel == Log.WARN && currentLogLevel >= LOG_LEVEL_NORMAL) - Log.w(tag, message); - else if(logLevel == Log.INFO && currentLogLevel >= LOG_LEVEL_NORMAL) - Log.i(tag, message); - else if(logLevel == Log.DEBUG && currentLogLevel >= LOG_LEVEL_DEBUG) - Log.d(tag, message); - else if(logLevel == Log.VERBOSE && currentLogLevel >= LOG_LEVEL_VERBOSE) - Log.v(tag, message); - } - - static public void logError(Context context, String tag, String message) { - logMesssage(context, Log.ERROR, tag, message); - } - - static public void logError(Context context, String message) { - logMesssage(context, Log.ERROR, DEFAULT_LOG_TAG, message); - } - - static public void logWarn(Context context, String tag, String message) { - logMesssage(context, Log.WARN, tag, message); - } - - static public void logWarn(Context context, String message) { - logMesssage(context, Log.WARN, DEFAULT_LOG_TAG, message); - } - - static public void logInfo(Context context, String tag, String message) { - logMesssage(context, Log.INFO, tag, message); - } - - static public void logInfo(Context context, String message) { - logMesssage(context, Log.INFO, DEFAULT_LOG_TAG, message); - } - - static public void logDebug(Context context, String tag, String message) { - logMesssage(context, Log.DEBUG, tag, message); - } - - static public void logDebug(Context context, String message) { - logMesssage(context, Log.DEBUG, DEFAULT_LOG_TAG, message); - } - - static public void logVerbose(Context context, String tag, String message) { - logMesssage(context, Log.VERBOSE, tag, message); - } - - static public void logVerbose(Context context, String message) { - logMesssage(context, Log.VERBOSE, DEFAULT_LOG_TAG, message); - } - - static public void logErrorAndShowToast(Context context, String tag, String message) { - if (context == null) return; - - if(getLogLevel(context) >= LOG_LEVEL_NORMAL) { - logError(context, tag, message); - showToast(context, message); - } - } - - static public void logErrorAndShowToast(Context context, String message) { - logErrorAndShowToast(context, DEFAULT_LOG_TAG, message); - } - - static public void logDebugAndShowToast(Context context, String tag, String message) { - if (context == null) return; - - if(getLogLevel(context) >= LOG_LEVEL_DEBUG) { - logDebug(context, tag, message); - showToast(context, message); - } - } - - static public void logDebugAndShowToast(Context context, String message) { - logDebugAndShowToast(context, DEFAULT_LOG_TAG, message); - } - - static public void logStackTrace(Context context, String tag, String message, Exception e) { - if (context == null) return; - - if(getLogLevel(context) >= LOG_LEVEL_NORMAL) - { - try { - StringWriter errors = new StringWriter(); - PrintWriter pw = new PrintWriter(errors); - e.printStackTrace(pw); - pw.close(); - if(message != null) - Log.e(tag, message + ":\n" + errors.toString()); - else - Log.e(tag, errors.toString()); - errors.close(); - } catch (IOException e1) { - e1.printStackTrace(); - } - } - } - - static public void logStackTrace(Context context, String tag, Exception e) { - logStackTrace(context, tag, null, e); - } - - static public void logStackTrace(Context context, Exception e) { - logStackTrace(context, DEFAULT_LOG_TAG, null, e); - } - - static public void showToast(final Context context, final String toastText) { - if (context == null) return; - - new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(context, toastText, Toast.LENGTH_LONG).show()); - } - - public static void showSetLogLevelDialog(final Context context) { - if (context == null) return; - - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(context.getString(R.string.log_level_title)); - - String[] logLevels = { - getLogLevelLabel(context, LOG_LEVEL_OFF, true), - getLogLevelLabel(context, LOG_LEVEL_NORMAL, true), - getLogLevelLabel(context, LOG_LEVEL_DEBUG, true), - getLogLevelLabel(context, LOG_LEVEL_VERBOSE, true) - }; - - int currentLogLevel = getLogLevel(context); - builder.setSingleChoiceItems(logLevels, currentLogLevel, (dialog, logLevel) -> { - switch (logLevel) { - case 0: setLogLevel(context, LOG_LEVEL_OFF); break; - case 1: setLogLevel(context, LOG_LEVEL_NORMAL); break; - case 2: setLogLevel(context, LOG_LEVEL_DEBUG); break; - case 3: setLogLevel(context, LOG_LEVEL_VERBOSE); break; - default: break; - } - dialog.dismiss(); - }); - - AlertDialog dialog = builder.create(); - dialog.show(); - } - - static public int getLogLevel(Context context) { - return QueryPreferences.getLogLevel(context); - } - - static public int getLogLevelFromFile(Context context) { - return QueryPreferences.getLogLevelFromFile(context); - } - - public static String getLogLevelLabel(final Context context, final int logLevel, final boolean addDefaultTag) { - String logLabel; - switch (logLevel) { - case LOG_LEVEL_OFF: logLabel = context.getString(R.string.log_level_off); break; - case LOG_LEVEL_NORMAL: logLabel = context.getString(R.string.log_level_normal); break; - case LOG_LEVEL_DEBUG: logLabel = context.getString(R.string.log_level_debug); break; - case LOG_LEVEL_VERBOSE: logLabel = context.getString(R.string.log_level_verbose); break; - default: logLabel = context.getString(R.string.log_level_unknown); break; - } - - if (addDefaultTag && logLevel == DEFAULT_LOG_LEVEL) - return logLabel + " (default)"; - else - return logLabel; - } - - static public void setLogLevel(Context context, int logLevel) { - QueryPreferences.setLogLevel(context, logLevel); - showToast(context, context.getString(R.string.log_level_value, getLogLevelLabel(context, getLogLevel(context), false))); - } - -} diff --git a/app/src/main/java/com/termux/tasker/utils/LoggerUtils.java b/app/src/main/java/com/termux/tasker/utils/LoggerUtils.java new file mode 100644 index 0000000..59506a8 --- /dev/null +++ b/app/src/main/java/com/termux/tasker/utils/LoggerUtils.java @@ -0,0 +1,62 @@ +package com.termux.tasker.utils; + +import android.content.Context; + +import androidx.appcompat.app.AlertDialog; + +import com.termux.shared.termux.settings.preferences.TermuxTaskerAppSharedPreferences; +import com.termux.tasker.R; +import com.termux.shared.logger.Logger; + +import static com.termux.shared.logger.Logger.getLogLevelLabel; + +public class LoggerUtils { + + public static void showSetLogLevelDialog(final Context context) { + if (context == null) return; + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(context.getString(com.termux.shared.R.string.log_level_title)); + + String[] logLevels = { + getLogLevelLabel(context, Logger.LOG_LEVEL_OFF, true), + getLogLevelLabel(context, Logger.LOG_LEVEL_NORMAL, true), + getLogLevelLabel(context, Logger.LOG_LEVEL_DEBUG, true), + getLogLevelLabel(context, Logger.LOG_LEVEL_VERBOSE, true) + }; + + TermuxTaskerAppSharedPreferences preferences = TermuxTaskerAppSharedPreferences.build(context); + if (preferences == null) return; + + int currentLogLevel = preferences.getLogLevel(true); + + builder.setSingleChoiceItems(logLevels, currentLogLevel, (dialog, logLevelIndex) -> { + int logLevel; + switch (logLevelIndex) { + case 0: + logLevel = Logger.LOG_LEVEL_OFF; + break; + case 1: + logLevel = Logger.LOG_LEVEL_NORMAL; + break; + case 2: + logLevel = Logger.LOG_LEVEL_DEBUG; + break; + case 3: + logLevel = Logger.LOG_LEVEL_VERBOSE; + break; + default: + logLevel = Logger.DEFAULT_LOG_LEVEL; + break; + } + + preferences.setLogLevel(context, logLevel, true); + + dialog.dismiss(); + }); + + AlertDialog dialog = builder.create(); + dialog.show(); + } + +} diff --git a/app/src/main/java/com/termux/tasker/utils/PluginUtils.java b/app/src/main/java/com/termux/tasker/utils/PluginUtils.java index 27ad656..0386079 100644 --- a/app/src/main/java/com/termux/tasker/utils/PluginUtils.java +++ b/app/src/main/java/com/termux/tasker/utils/PluginUtils.java @@ -1,42 +1,480 @@ package com.termux.tasker.utils; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.Context; +import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; -import com.termux.tasker.Constants; +import com.termux.shared.data.DataUtils; +import com.termux.shared.errors.Errno; +import com.termux.shared.file.FileUtils; +import com.termux.shared.logger.Logger; +import com.termux.shared.shell.command.ExecutionCommand; +import com.termux.shared.shell.command.result.ResultConfig; +import com.termux.shared.shell.command.result.ResultData; +import com.termux.shared.shell.command.result.ResultSender; +import com.termux.shared.shell.command.runner.app.AppShell; +import com.termux.shared.termux.settings.preferences.TermuxPreferenceConstants.TERMUX_TASKER_APP; +import com.termux.shared.termux.settings.preferences.TermuxTaskerAppSharedPreferences; +import com.termux.shared.termux.shell.command.runner.terminal.TermuxSession; +import com.termux.tasker.FireReceiver; +import com.termux.tasker.PluginResultsService; +import com.termux.shared.termux.TermuxConstants; +import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE; +import com.termux.shared.settings.properties.SharedProperties; import com.termux.tasker.R; +import java.util.regex.Pattern; + +/** + * A util class to handle sending of plugin commands to the execution service, + * processing of the result of plugin commands received back from the execution service and + * sending immediate or pending results back to the plugin host. + * + * This is currently designed and tested with Tasker as the plugin host app and the + * {@link TermuxConstants.TERMUX_APP.TERMUX_SERVICE} as the execution service of plugin + * commands but should work with other plugin host apps. + * The {@link TermuxConstants.TERMUX_APP.TERMUX_SERVICE} will run the commands with the + * {@link AppShell} class if background mode is enabled and + * {@link TermuxSession} if foreground terminal mode is enabled. The + * result of commands is only returned if the plugin action bundle has + * {@link com.termux.tasker.PluginBundleManager#EXTRA_WAIT_FOR_RESULT} set to true. + * + * Flow to be used for the commands received by {@link FireReceiver}: + * 1. Call {@link #sendExecuteIntentToExecuteService} from {@link FireReceiver}. This expects the + * original intent received by {@link FireReceiver}, the execution intent that should be sent to + * call the execution service containing its {@link ComponentName} and any extras required to run the + * plugin commands, and a boolean for whether commands are to be run in background mode or not. + * If the plugin action has a timeout greater than 0 and result is to be returned, then the + * function will automatically create a {@link android.app.PendingIntent} that can be used to + * return the results back to {@link PluginResultsService} and add the original + * {@link android.content.Intent} received by {@link FireReceiver} as {@link #EXTRA_ORIGINAL_INTENT} + * to it and then add the created {@link android.app.PendingIntent} to the execution intent as + * {@link TERMUX_SERVICE#EXTRA_PENDING_INTENT} extra. + * Otherwise, the function immediately returns {@link TaskerPlugin.Setting#RESULT_CODE_OK} + * back to the plugin host app using the {@link #sendImmediateResultToPluginHostApp} function and + * the flow ends here for the usage of {@link FireReceiver}. + * + * 2. The execution service should receive the execution intent and run the required plugin commands. + * If background mode is enabled, then results should be returned back via the + * {@link android.app.PendingIntent} received, which in this case will be meant for the + * {@link PluginResultsService}. The send the result back, the execution service can send the result + * intent with {@link ResultSender#sendCommandResultDataWithPendingIntent(Context, String, String, ResultConfig, ResultData, boolean)}, + * like TermuxService does via com.termux.app.utils.PluginUtils.processPluginExecutionCommandResult(). + * The pending intent should already contain the original {@link android.content.Intent} received by the + * {@link FireReceiver}. A result {@link Bundle} object with the {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE} + * key should also be sent back in an {@link android.content.Intent} using the + * {@link android.app.PendingIntent#send(Context, int, Intent)} function. The bundle can contain + * the keys whose values will be sent back to the plugin host app: + * {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT} (String), + * {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH} (String) + * {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_STDERR} (String), + * {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH} (String), + * {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE} (Integer), + * {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_ERR} (Integer) and + * {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG} (String) + * + * 3. The {@link android.app.PendingIntent} sent is received by the {@link PluginResultsService} + * with the onHandleIntent function which calls the + * {@link #sendPendingResultToPluginHostApp} function with the intent received. + * + * 4. The {@link #sendPendingResultToPluginHostApp} function extracts the original intent and the + * result bundle from the intent received and calls the {@link #createVariablesBundle} function + * to create the variables bundle to be sent back to plugin host and then sends it with + * {@link TaskerPlugin.Setting#signalFinish} function. If any of the Integer keys + * do not exist in the result bundle or if the values of String keys are null or empty in the result + * bundle, then they are not sent back to the plugin host. The flow ends here. + * + * The {@link #sendImmediateResultToPluginHostApp} function can be used to send result back to the + * plugin host immediately like in case there is an error processing the plugin command request or + * if the result should not be expected to be sent back by the execution service. + * + * The {@link #createVariablesBundle} function creates a variables bundle that can be sent back to + * the plugin host. The bundle will contain the keys: + * {@link #PLUGIN_VARIABLE_STDOUT} (String), + * {@link #PLUGIN_VARIABLE_STDOUT_ORIGINAL_LENGTH} (String), + * {@link #PLUGIN_VARIABLE_STDERR} (String), + * {@link #PLUGIN_VARIABLE_STDERR_ORIGINAL_LENGTH} (String), + * {@link #PLUGIN_VARIABLE_EXIT_CODE} (String) + * and {@link #PLUGIN_VARIABLE_ERRMSG} (String). + * + * The {@link #PLUGIN_VARIABLE_ERRMSG} key will only be added if the {@link #PLUGIN_VARIABLE_ERR} value + * to be sent back to the plugin host is greater than {@link TaskerPlugin.Setting#RESULT_CODE_OK}. + * Any null or empty values are not added to the variables bundle. + * + * The value for {@link #PLUGIN_VARIABLE_ERR} is first sanitized by the {@link #sanitizeErrCode} + * function before it is sent back to the plugin host. + */ public class PluginUtils { + /** Plugin variable for stdout value of termux command */ + public static final String PLUGIN_VARIABLE_STDOUT = "%stdout"; // Default: "%stdout" + /** Plugin variable for original length of stdout value of termux command */ + public static final String PLUGIN_VARIABLE_STDOUT_ORIGINAL_LENGTH = "%stdout_original_length"; // Default: "%stdout_original_length" + /** Plugin variable for stderr value of termux command */ + public static final String PLUGIN_VARIABLE_STDERR = "%stderr"; // Default: "%stderr" + /** Plugin variable for original length of stderr value of termux command */ + public static final String PLUGIN_VARIABLE_STDERR_ORIGINAL_LENGTH = "%stderr_original_length"; // Default: "%stderr_original_length" + /** Plugin variable for exit code value of termux command */ + public static final String PLUGIN_VARIABLE_EXIT_CODE = "%result"; // Default: "%result" + /** Plugin variable for err value of termux command */ + public static final String PLUGIN_VARIABLE_ERR = "%err"; // Default: "%err" + /** Plugin variable for errmsg value of termux command */ + public static final String PLUGIN_VARIABLE_ERRMSG = "%errmsg"; // Default: "%errmsg" + + /** Intent {@code Parcelable} extra containing original intent received from plugin host app by FireReceiver */ + public static final String EXTRA_ORIGINAL_INTENT = "originalIntent"; // Default: "originalIntent" + + /** + * A regex to validate if a string matches a valid plugin host variable name with the percent sign "%" prefix. + * Valid values: A string containing a percent sign character "%", followed by 1 alphanumeric character, + * followed by 2 or more alphanumeric or underscore "_" characters but does not end with an underscore "_" + */ + public static final String PLUGIN_HOST_VARIABLE_NAME_MATCH_EXPRESSION = "%[a-zA-Z0-9][a-zA-Z0-9_]{2,}(? 0): `" + (receiver != null && receiver.isOrderedBroadcast()) + "`"); + + // If timeout for plugin action is greater than 0 and plugin action should wait for results + waitForResult = (receiver != null && receiver.isOrderedBroadcast() && waitForResult); + Logger.logDebug(LOG_TAG, "Sending execution intent to " + executionIntent.getComponent().toString() + (waitForResult ? " and " : " without ") + "waiting for result"); + + if (waitForResult) { + // Notify plugin host app that result will be sent later + // Result should be sent to PluginResultsService via a PendingIntent by execution service + // after commands have finished executing + receiver.setResultCode(TaskerPlugin.Setting.RESULT_CODE_PENDING); + + // Create intent for PluginResultsService class and add original intent received by + // FireReceiver to it + Intent pluginResultsServiceIntent = new Intent(context, PluginResultsService.class); + pluginResultsServiceIntent.putExtra(EXTRA_ORIGINAL_INTENT, originalIntent); + + // Create PendingIntent that can be used by execution service to send result of commands + // back to PluginResultsService + PendingIntent pendingIntent = PendingIntent.getService(context, getLastPendingIntentRequestCode(context), pluginResultsServiceIntent, + PendingIntent.FLAG_ONE_SHOT | (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_MUTABLE : 0)); + executionIntent.putExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT, pendingIntent); + } else { + // If execution result is not to be returned, do not expect results back from the + // execution service and return result now so that plugin action does not timeout + sendImmediateResultToPluginHostApp(receiver, originalIntent, TaskerPlugin.Setting.RESULT_CODE_OK, null); } try { - return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionCode; - } catch (final UnsupportedOperationException e) { - // This exception is thrown by test contexts. - return 1; - } catch (final Exception e) { - throw new RuntimeException(e); + // Send execution intent to execution service + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // https://developer.android.com/about/versions/oreo/background.html + context.startForegroundService(executionIntent); + } else { + context.startService(executionIntent); + } + } catch (Exception e) { + String errmsg = Logger.getMessageAndStackTraceString("Failed to send execution intent to " + executionIntent.getComponent().toString(), e); + Logger.logErrorAndShowToast(context, LOG_TAG, errmsg); + PluginUtils.sendImmediateResultToPluginHostApp(receiver, originalIntent, TaskerPlugin.Setting.RESULT_CODE_FAILED, errmsg); } } /** - * Check if package has {@link com.termux.tasker.Constants#PERMISSION_RUN_COMMAND}. + * Send immediate result to plugin host app in a variables bundle. + * + * @param receiver The {@link BroadcastReceiver} of the originalIntent. + * @param originalIntent The original {@link Intent} received by {@link FireReceiver}. + * @param errCode The value for {@link #PLUGIN_VARIABLE_ERR} variable of plugin action. + * @param errmsg The value for {@link #PLUGIN_VARIABLE_ERRMSG} variable of plugin action. + */ + public static void sendImmediateResultToPluginHostApp(final BroadcastReceiver receiver, final Intent originalIntent, + final int errCode, final String errmsg) { + sendImmediateResultToPluginHostApp(receiver, originalIntent, null, null, + null, null, null, errCode, errmsg); + } + + /** + * Send immediate result to plugin host app in a variables bundle. * - * @param context to get error string. - * @param packageName to check. - * @return errmsg if package has not been granted - * {@link com.termux.tasker.Constants#PERMISSION_RUN_COMMAND}, otherwise null. + * @param receiver The {@link BroadcastReceiver} of the originalIntent. + * @param originalIntent The original {@link Intent} received by {@link FireReceiver}. + * @param stdout The value for {@link #PLUGIN_VARIABLE_STDOUT} variable of plugin action. + * @param stdoutOriginalLength The value for {@link #PLUGIN_VARIABLE_STDOUT_ORIGINAL_LENGTH} + * variable of plugin action. + * @param stderr The value for {@link #PLUGIN_VARIABLE_STDERR} variable of plugin action. + * @param stderrOriginalLength The value for {@link #PLUGIN_VARIABLE_STDERR_ORIGINAL_LENGTH} + * variable of plugin action. + * @param exitCode The value for {@link #PLUGIN_VARIABLE_EXIT_CODE} variable of plugin action. + * @param errCode The value for {@link #PLUGIN_VARIABLE_ERR} variable of plugin action. + * @param errmsg The value for {@link #PLUGIN_VARIABLE_ERRMSG} variable of plugin action. + */ + public static void sendImmediateResultToPluginHostApp(final BroadcastReceiver receiver, final Intent originalIntent, + final String stdout, String stdoutOriginalLength, + final String stderr, String stderrOriginalLength, + final String exitCode, final int errCode, final String errmsg) { + if (receiver == null) return; + + // If timeout for plugin action is 0, then don't send anything + if (!receiver.isOrderedBroadcast()) return; + + int err = sanitizeErrCode(errCode); + + Logger.logInfo(LOG_TAG, "Sending immediate result to plugin host app. " + PLUGIN_VARIABLE_ERR + ": " + ((err == TaskerPlugin.Setting.RESULT_CODE_OK) ? "success" : "failed") + " (" + err + ")"); + + if (TaskerPlugin.Setting.hostSupportsVariableReturn(originalIntent.getExtras())) { + final Bundle varsBundle = createVariablesBundle(stdout, stdoutOriginalLength, + stderr, stderrOriginalLength, exitCode, err, errmsg); + TaskerPlugin.addVariableBundle(receiver.getResultExtras(true), varsBundle); + } + + receiver.setResultCode(err); + } + + /** + * Send pending result to plugin host app in a variables bundle. + * + * @param context The {@link Context} that will be used to send variables bundle to plugin host app and logging. + * @param intent The {@link Intent} containing result and original intent received by {@link FireReceiver}. + */ + public static void sendPendingResultToPluginHostApp(final Context context, final Intent intent) { + if (intent == null){ + Logger.logWarn(LOG_TAG, "Ignoring null intent passed to sendPendingResultToPluginHostApp()."); + return; + } + + final Intent originalIntent = intent.getParcelableExtra(EXTRA_ORIGINAL_INTENT); + if (originalIntent == null) { + Logger.logError(LOG_TAG, "The intent passed to sendPendingResultToPluginHostApp() must contain the original intent received by the FireReceiver at the " + EXTRA_ORIGINAL_INTENT + " key."); + return; + } + + final Bundle resultBundle = intent.getBundleExtra(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE); + if (resultBundle == null) { + Logger.logError(LOG_TAG, "The intent passed to sendPendingResultToPluginHostApp() must contain the result bundle at the " + TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE + " key."); + return; + } + + int err = TaskerPlugin.Setting.RESULT_CODE_OK; + // This check is necessary, otherwise default value will be 0 if extra does not exist, + // and so plugin host app like Tasker will consider the action as failed for the value 0, + // since it equals Activity.RESULT_CANCELED (0) instead of TaskerPlugin.Setting.RESULT_CODE_OK/Activity.RESULT_OK (-1) + if (resultBundle.containsKey(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERR)) + err = sanitizeErrCode(resultBundle.getInt(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERR)); + + Logger.logInfo(LOG_TAG, "Sending pending result to plugin host app. " + PLUGIN_VARIABLE_ERR + ": " + ((err == TaskerPlugin.Setting.RESULT_CODE_OK) ? "success" : "failed") + " (" + err + ")"); + + String exitCode = null; + if (resultBundle.containsKey(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE)) + exitCode = Integer.toString(resultBundle.getInt(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE)); + + final Bundle varsBundle = createVariablesBundle( + resultBundle.getString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT, ""), + resultBundle.getString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH, ""), + resultBundle.getString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR, ""), + resultBundle.getString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH, ""), + exitCode, err, resultBundle.getString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG, "")); + + if(context != null) + TaskerPlugin.Setting.signalFinish(context, originalIntent, err, varsBundle); + } + + /** + * Process {@link ExecutionCommand} error. + * + * The ExecutionCommand currentState must be equal to {@link ExecutionCommand.ExecutionState#FAILED}. + * The {@link ResultData#getErrCode()} must have been set to a value greater than + * {@link Errno#ERRNO_SUCCESS}. + * The {@link ResultData#errorsList} must also be set with appropriate error info. + * + * @param context The {@link Context} for operations. + * @param receiver The {@link BroadcastReceiver} of the originalIntent. + * @param originalIntent The {@link Intent} received by {@link FireReceiver}. + * @param logTag The log tag to use for logging. + * @param executionCommand The {@link ExecutionCommand} that failed. + * @param errCode The value for {@link #PLUGIN_VARIABLE_ERR} variable of plugin action. + */ + public static void processPluginExecutionCommandError(final Context context, final BroadcastReceiver receiver, final Intent originalIntent, String logTag, final ExecutionCommand executionCommand, final int errCode) { + if (context == null || executionCommand == null) return; + + logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG); + + if (!executionCommand.isStateFailed()) { + Logger.logWarn(logTag, executionCommand.getCommandIdAndLabelLogString() + ": Ignoring call to processPluginExecutionCommandError() since the execution command is not in ExecutionState.FAILED"); + return; + } + + boolean isExecutionCommandLoggingEnabled = Logger.shouldEnableLoggingForCustomLogLevel(executionCommand.backgroundCustomLogLevel); + + // Log the error and any exception + Logger.logErrorExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, + true, isExecutionCommandLoggingEnabled)); + + PluginUtils.sendImmediateResultToPluginHostApp(receiver, originalIntent, + errCode, ResultData.getErrorsListMinimalString(executionCommand.resultData)); + } + + + + /** + * Create variables bundle to send back to plugin host app. + * + * @param stdout The value for {@link #PLUGIN_VARIABLE_STDOUT} variable of plugin action. + * @param stdoutOriginalLength The value for {@link #PLUGIN_VARIABLE_STDOUT_ORIGINAL_LENGTH} + * variable of plugin action. + * @param stderr The value for {@link #PLUGIN_VARIABLE_STDERR} variable of plugin action. + * @param stderrOriginalLength The value for {@link #PLUGIN_VARIABLE_STDERR_ORIGINAL_LENGTH} + * variable of plugin action. + * @param exitCode The value for {@link #PLUGIN_VARIABLE_EXIT_CODE} variable of plugin action. + * @param errCode The value for {@link #PLUGIN_VARIABLE_ERR} variable of plugin action. + * @param errmsg The value for {@link #PLUGIN_VARIABLE_ERRMSG} variable of plugin action. + * @return Returns the variables {@code Bundle}. + */ + public static Bundle createVariablesBundle(String stdout, String stdoutOriginalLength, + String stderr, String stderrOriginalLength, + String exitCode, int errCode, String errmsg) { + + Logger.logDebugExtended(LOG_TAG, "Variables bundle for plugin host app:\n" + + PLUGIN_VARIABLE_STDOUT + ": `" + stdout + "`\n" + + PLUGIN_VARIABLE_STDOUT_ORIGINAL_LENGTH + ": `" + stdoutOriginalLength + "`\n" + + PLUGIN_VARIABLE_STDERR + ": `" + stderr + "`\n" + + PLUGIN_VARIABLE_STDERR_ORIGINAL_LENGTH + ": `" + stderrOriginalLength + "`\n" + + PLUGIN_VARIABLE_EXIT_CODE + ": `" + exitCode + "`\n" + + PLUGIN_VARIABLE_ERR + ": `" + errCode + "`\n" + + PLUGIN_VARIABLE_ERRMSG + ": `" + errmsg + "`"); + + if (errCode == TaskerPlugin.Setting.RESULT_CODE_OK && errmsg != null && !errmsg.isEmpty()) { + Logger.logWarn(LOG_TAG, "Ignoring setting " + PLUGIN_VARIABLE_ERRMSG + " variable since " + PLUGIN_VARIABLE_ERR + " is set to RESULT_CODE_OK \"" + TaskerPlugin.Setting.RESULT_CODE_OK + "\", " + PLUGIN_VARIABLE_ERRMSG + ": \"" + errmsg + "\""); + errmsg = ""; + } + + // Send back empty values for variables not to be returned. + // This will/should unset their respective variables in the plugin host app, + // since if multiple actions are run in the same task, some variables from previous actions + // may still be set and get mixed in with current ones. + if (stdout == null) stdout = ""; + if (stdoutOriginalLength == null) stdoutOriginalLength = ""; + if (stderr == null) stderr = ""; + if (stderrOriginalLength == null) stderrOriginalLength = ""; + if (exitCode == null) exitCode = ""; + if (errmsg == null) errmsg = ""; + + final Bundle variablesBundle = new Bundle(); + + if (isPluginHostAppVariableNameValid(PLUGIN_VARIABLE_STDOUT)) + variablesBundle.putString(PLUGIN_VARIABLE_STDOUT, stdout); + if (isPluginHostAppVariableNameValid(PLUGIN_VARIABLE_STDOUT_ORIGINAL_LENGTH)) + variablesBundle.putString(PLUGIN_VARIABLE_STDOUT_ORIGINAL_LENGTH, stdoutOriginalLength); + if (isPluginHostAppVariableNameValid(PLUGIN_VARIABLE_STDERR)) + variablesBundle.putString(PLUGIN_VARIABLE_STDERR, stderr); + if (isPluginHostAppVariableNameValid(PLUGIN_VARIABLE_STDERR_ORIGINAL_LENGTH)) + variablesBundle.putString(PLUGIN_VARIABLE_STDERR_ORIGINAL_LENGTH, stderrOriginalLength); + if (isPluginHostAppVariableNameValid(PLUGIN_VARIABLE_EXIT_CODE)) + variablesBundle.putString(PLUGIN_VARIABLE_EXIT_CODE, exitCode); + if (isPluginHostAppVariableNameValid(PLUGIN_VARIABLE_ERRMSG)) + variablesBundle.putString(PLUGIN_VARIABLE_ERRMSG, errmsg); + + return variablesBundle; + } + + /** + * Sanitize errCode value so that it can be sent back to plugin host app as %err value. + * For custom result codes for the plugin, start numbering from + * {@link TaskerPlugin.Setting#RESULT_CODE_FAILED_PLUGIN_FIRST}, + * otherwise plugin host app like Tasker will consider them as unknown result codes. + * + * @param errCode The value to sanitize. + * @return Returns {@code errCode} value as is if valid, otherwise returns + * {@link TaskerPlugin.Setting#RESULT_CODE_OK}. + */ + public static int sanitizeErrCode(final int errCode) { + int err; + if (errCode >= TaskerPlugin.Setting.RESULT_CODE_OK) { + err = errCode; + } else { + Logger.logWarn(LOG_TAG, "Ignoring invalid " + PLUGIN_VARIABLE_ERR + " value \"" + errCode + "\" for plugin action and force setting it to RESULT_CODE_OK \"" + TaskerPlugin.Setting.RESULT_CODE_OK + "\""); + err = TaskerPlugin.Setting.RESULT_CODE_OK; + } + + return err; + } + + + + /** + * Checks if plugin host variable name is valid and can be sent back to plugin host app. + * + * @param name The {@code name} of the plugin variable. + * @return Returns {@code true} if valid, otherwise {@code false}. + */ + public static boolean isPluginHostAppVariableNameValid(final String name) { + if (name == null || name.isEmpty() || !TaskerPlugin.variableNameValid(name)) { + Logger.logWarn(LOG_TAG, "Ignoring invalid plugin variable name: \"" + name + "\""); + return false; + } + + return true; + } + + /** + * Determines whether string exactly matches a valid plugin host app variable. + * + * @param string The {@link String} to check. + * @return Returns {@code true} if string exactly matches a plugin host app variable, otherwise {@code false}. + */ + public static boolean isPluginHostAppVariableString(String string) { + if (string == null || string.isEmpty()) return false; + return Pattern.compile("^" + PLUGIN_HOST_VARIABLE_NAME_MATCH_EXPRESSION + "$", 0).matcher(string).matches(); + } + + /** + * Determines whether string contains a plugin host app variable. + * + * @param string The {@link String} to check. + * @return Returns {@code true} if string contains a plugin host app variable, otherwise {@code false}. + */ + public static boolean isPluginHostAppVariableContainingString(String string) { + if (string == null || string.isEmpty()) return false; + return Pattern.compile(".*" + PLUGIN_HOST_VARIABLE_NAME_MATCH_EXPRESSION + ".*", 0).matcher(string).matches(); + } + + + + /** + * Check if package has {@link TermuxConstants#PERMISSION_RUN_COMMAND}. + * + * @param context The {@link Context} to get error string. + * @param packageName The package name to check. + * @return Returns the {@code errmsg} if package has not been granted + * {@link TermuxConstants#PERMISSION_RUN_COMMAND}, otherwise {@code null}. */ public static String checkIfPackageHasPermissionRunCommand(final Context context, final String packageName) { @@ -45,37 +483,66 @@ public static String checkIfPackageHasPermissionRunCommand(final Context context // Check if packageName has been granted PERMISSION_RUN_COMMAND PackageManager packageManager = context.getPackageManager(); // If permission not granted - if (packageManager.checkPermission(Constants.PERMISSION_RUN_COMMAND, packageName) != PackageManager.PERMISSION_GRANTED) { + if (packageManager.checkPermission(TermuxConstants.PERMISSION_RUN_COMMAND, packageName) != PackageManager.PERMISSION_GRANTED) { ApplicationInfo applicationInfo; try { applicationInfo = packageManager.getApplicationInfo(packageName, 0); } catch (final PackageManager.NameNotFoundException e) { applicationInfo = null; } - final String appName = (String) (applicationInfo != null ? packageManager.getApplicationLabel(applicationInfo) : context.getString(R.string.unknown_app)); - errmsg = context.getString(R.string.plugin_permission_ungranted_warning, appName, packageName, Constants.PERMISSION_RUN_COMMAND); + final String appName = (String) (applicationInfo != null ? packageManager.getApplicationLabel(applicationInfo) : context.getString(R.string.error_unknown_app)); + errmsg = context.getString(R.string.error_plugin_permission_ungranted_warning, appName, packageName, TermuxConstants.PERMISSION_RUN_COMMAND); } return errmsg; } /** - * Check if executable is not in {@link Constants#TASKER_PATH} and - * {@link Constants#ALLOW_EXTERNAL_APPS_PROPERTY} property is not set to "true". + * Check if executable is not under {@link TermuxConstants#TERMUX_TASKER_SCRIPTS_DIR_PATH} and + * {@link TermuxConstants#PROP_ALLOW_EXTERNAL_APPS} property is not set to "true". * - * @param context to get error string. - * @param executable path to check. - * @return errmsg if policy is violated, otherwise null. + * @param context The {@link Context} to get error string. + * @param executable The {@code path} to check. + * @return Returns the {@code errmsg} if policy is violated, otherwise {@code null}. */ - public static String checkIfAllowExternalAppsPolicyIsViolated(final Context context, final String executable) { - if (executable == null || executable.isEmpty()) return context.getString(R.string.null_or_empty_executable); + public static String checkIfTermuxTaskerAllowExternalAppsPolicyIsViolated(final Context context, final String executable) { + if (executable == null || executable.isEmpty()) return context.getString(R.string.error_null_or_empty_executable); String errmsg = null; - if (!FileUtils.isPathInTaskerDir(executable) && !TermuxAppUtils.isAllowExternalApps(context)) { - errmsg = context.getString(R.string.allow_external_apps_ungranted_warning); + if (!FileUtils.isPathInDirPath(executable, TermuxConstants.TERMUX_TASKER_SCRIPTS_DIR_PATH, true) && + !SharedProperties.isPropertyValueTrue(context, + SharedProperties.getPropertiesFileFromList(TermuxConstants.TERMUX_PROPERTIES_FILE_PATHS_LIST, LOG_TAG), + TermuxConstants.PROP_ALLOW_EXTERNAL_APPS, true)) { + errmsg = context.getString(R.string.error_allow_external_apps_ungranted_warning); } return errmsg; } + + /** + * Try to get the next unique {@link PendingIntent} request code that isn't already being used by + * the app and which would create a unique {@link PendingIntent} that doesn't conflict with that + * of any other execution commands. + * + * @param context The {@link Context} for operations. + * @return Returns the request code that should be safe to use. + */ + public synchronized static int getLastPendingIntentRequestCode(final Context context) { + if (context == null) return TERMUX_TASKER_APP.DEFAULT_VALUE_KEY_LAST_PENDING_INTENT_REQUEST_CODE; + + TermuxTaskerAppSharedPreferences preferences = TermuxTaskerAppSharedPreferences.build(context); + if (preferences == null) return TERMUX_TASKER_APP.DEFAULT_VALUE_KEY_LAST_PENDING_INTENT_REQUEST_CODE; + + int lastPendingIntentRequestCode = preferences.getLastPendingIntentRequestCode(); + + int nextPendingIntentRequestCode = lastPendingIntentRequestCode + 1; + + if (nextPendingIntentRequestCode == Integer.MAX_VALUE || nextPendingIntentRequestCode < 0) + nextPendingIntentRequestCode = TERMUX_TASKER_APP.DEFAULT_VALUE_KEY_LAST_PENDING_INTENT_REQUEST_CODE; + + preferences.setLastPendingIntentRequestCode(nextPendingIntentRequestCode); + return nextPendingIntentRequestCode; + } + } diff --git a/app/src/main/java/com/termux/tasker/utils/QueryPreferences.java b/app/src/main/java/com/termux/tasker/utils/QueryPreferences.java deleted file mode 100644 index 8ba9dae..0000000 --- a/app/src/main/java/com/termux/tasker/utils/QueryPreferences.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.termux.tasker.utils; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; - -public class QueryPreferences { - - private static final String QUERY_PREFERENCES_FILENAME = "com.termux.tasker_preferences"; - - private static final String LOGGING_LEVEL = "log_level"; - - //get default SharedPreferences with MODE_PRIVATE - public static SharedPreferences getDefaultSharedPreferences(Context context) throws Exception { - return context.getSharedPreferences(QUERY_PREFERENCES_FILENAME, Context.MODE_PRIVATE); - } - - //get default SharedPreferences with MODE_PRIVATE | MODE_MULTI_PROCESS - public static SharedPreferences getDefaultSharedPreferencesMultiProcess(Context context) throws Exception { - return context.getSharedPreferences(QUERY_PREFERENCES_FILENAME, Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS); - } - - //gets value from SharedPreferencesImpl memory cache - public static int getLogLevel(Context context) { - try { - return getDefaultSharedPreferences(context) - .getInt(LOGGING_LEVEL, Logger.DEFAULT_LOG_LEVEL); - } - catch (Exception e) { - Logger.logStackTrace(context, "Error getting \"" + LOGGING_LEVEL + "\" from shared preferences", e); - return Logger.DEFAULT_LOG_LEVEL; - } - } - - //gets value from shared preferences file if updated, otherwise from SharedPreferencesImpl memory cache - public static int getLogLevelFromFile(Context context) { - try { - return getDefaultSharedPreferencesMultiProcess(context) - .getInt(LOGGING_LEVEL, Logger.DEFAULT_LOG_LEVEL); - } - catch (Exception e) { - Logger.logStackTrace(context, "Error getting \"" + LOGGING_LEVEL + "\" from shared preferences", e); - return Logger.DEFAULT_LOG_LEVEL; - } - } - - // sets value to shared preferences memory cache and file synchronously - @SuppressLint("ApplySharedPref") - public static void setLogLevel(Context context, int logLevel) { - if (context == null) return; - - try { - getDefaultSharedPreferences(context) - .edit() - .putInt(LOGGING_LEVEL, logLevel) - .commit(); //using commit() instead of apply() since app is multi-process, FireReceiver has tag android:process=":background" - } catch (Exception e) { - Logger.logStackTrace(context, "Error setting \"" + LOGGING_LEVEL + "\" to shared preferences", e); - } - } -} diff --git a/app/src/main/java/com/termux/tasker/TaskerPlugin.java b/app/src/main/java/com/termux/tasker/utils/TaskerPlugin.java similarity index 99% rename from app/src/main/java/com/termux/tasker/TaskerPlugin.java rename to app/src/main/java/com/termux/tasker/utils/TaskerPlugin.java index 8c46efe..d3a9db3 100644 --- a/app/src/main/java/com/termux/tasker/TaskerPlugin.java +++ b/app/src/main/java/com/termux/tasker/utils/TaskerPlugin.java @@ -1,4 +1,4 @@ -package com.termux.tasker; +package com.termux.tasker.utils; // Constants and functions for Tasker *extensions* to the plugin protocol // See Also: http://tasker.dinglisch.net/plugins.html diff --git a/app/src/main/java/com/termux/tasker/utils/TermuxAppUtils.java b/app/src/main/java/com/termux/tasker/utils/TermuxAppUtils.java deleted file mode 100644 index d70626c..0000000 --- a/app/src/main/java/com/termux/tasker/utils/TermuxAppUtils.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.termux.tasker.utils; - -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; - -import com.termux.tasker.Constants; -import com.termux.tasker.R; - -import java.io.File; -import java.io.FileInputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.Properties; - -public class TermuxAppUtils { - - /** - * Check if Termux app is installed and accessible. This is done by checking if - * {@link com.termux.tasker.Constants#TERMUX_PACKAGE} is installed and - * {@link com.termux.tasker.Constants#PREFIX_PATH} is a directory and has read and execute - * permissions. The {@link com.termux.tasker.Constants#PREFIX_PATH} directory would not exist - * if termux has not been installed or setup or {@link com.termux.tasker.Constants#PREFIX_PATH} - * was deleted by the user. - * - * @param context to get error string. - * @return errmsg if termux package is not installed, disabled or - * {@link com.termux.tasker.Constants#PREFIX_PATH} is not a directory, or does not have - * read or execute permissions, otherwise null. - */ - public static String checkIfTermuxAppIsInstalledAndAccessible(final Context context) { - - String errmsg = null; - - PackageManager packageManager = context.getPackageManager(); - - ApplicationInfo applicationInfo; - try { - applicationInfo = packageManager.getApplicationInfo(Constants.TERMUX_PACKAGE, 0); - } catch (final PackageManager.NameNotFoundException e) { - applicationInfo = null; - } - boolean termuxAppEnabled = (applicationInfo != null && applicationInfo.enabled); - - // If Termux app is not installed or is disabled - if (!termuxAppEnabled) { - errmsg = context.getString(R.string.termux_app_not_installed_or_disabled_warning); - } - // If Termux PREFIX_PATH is not a directory or does not have read or execute permissions - else if (!Constants.PREFIX_DIR.isDirectory() || !Constants.PREFIX_DIR.canRead() || !Constants.PREFIX_DIR.canExecute()) { - errmsg = context.getString(R.string.termux_app_prefix_path_inaccessible_warning); - } - - return errmsg; - } - - /** - * Get value of termux property in ~/.termux/termux.properties. - * - * @param context for logging. - * @param property name. - * @param defaultValue if property not found. - * . - * - * @return property value if it exists, otherwise defaultValue. - */ - public static String getTermuxProperty(final Context context, final String property, final String defaultValue) { - File propsFile = new File(Constants.HOME_PATH + "/.termux/termux.properties"); - if (!propsFile.exists()) - propsFile = new File(Constants.HOME_PATH + "/.config/termux/termux.properties"); - - Properties props = new Properties(); - try { - if (propsFile.isFile() && propsFile.canRead()) { - try (FileInputStream in = new FileInputStream(propsFile)) { - props.load(new InputStreamReader(in, StandardCharsets.UTF_8)); - } - } - } catch (Exception e) { - Logger.logStackTrace(context, "Error loading termux.properties", e); - } - - return props.getProperty(property, defaultValue); - } - - /** - * Determines whether {@link Constants#ALLOW_EXTERNAL_APPS_PROPERTY} property is set to "true" in - * ~/.termux/termux.properties. - * - * @param context for logging. - * @return true if property exists and value is "true", otherwise false. - */ - public static boolean isAllowExternalApps(final Context context) { - return getTermuxProperty(context, Constants.ALLOW_EXTERNAL_APPS_PROPERTY, Constants.ALLOW_EXTERNAL_APPS_PROPERTY_DEFAULT_VALUE).equals("true"); - } -} diff --git a/app/src/main/res/layout/activity_edit_configuration.xml b/app/src/main/res/layout/activity_edit_configuration.xml new file mode 100644 index 0000000..22bff83 --- /dev/null +++ b/app/src/main/res/layout/activity_edit_configuration.xml @@ -0,0 +1,375 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_termux_tasker_main.xml b/app/src/main/res/layout/activity_termux_tasker_main.xml new file mode 100644 index 0000000..d25faca --- /dev/null +++ b/app/src/main/res/layout/activity_termux_tasker_main.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/edit_activity.xml b/app/src/main/res/layout/edit_activity.xml deleted file mode 100644 index 8285842..0000000 --- a/app/src/main/res/layout/edit_activity.xml +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/menu/activity_edit_configuration.xml b/app/src/main/res/menu/activity_edit_configuration.xml new file mode 100644 index 0000000..bbce493 --- /dev/null +++ b/app/src/main/res/menu/activity_edit_configuration.xml @@ -0,0 +1,14 @@ + + +

+ + + + + diff --git a/app/src/main/res/menu/edit_activity.xml b/app/src/main/res/menu/edit_activity.xml deleted file mode 100644 index 19171bf..0000000 --- a/app/src/main/res/menu/edit_activity.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 7b4d74a..6252451 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -3,4 +3,8 @@ 16dp 16dp + 8dp + 16dp + 8dp + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index da0ff04..2a6b959 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,55 +1,72 @@ + + + + + + + + ]> + - Termux:Tasker + &TERMUX_TASKER_APP_NAME; + &TERMUX_APP_NAME; user + &TERMUX_TASKER_APP_NAME; is a plugin app for the &TERMUX_APP_NAME; app + that allows &TERMUX_APP_NAME; commands to be executed from Tasker and other plugin host apps. + Check &TERMUX_APP_NAME; app github %1$s and &TERMUX_TASKER_APP_NAME; app github %2$s for more info. + Visit %1$s for more info on plugin usage. + + + The &TERMUX_TASKER_APP_NAME; app does not + require a require launcher activity/icon to function. You can optionally disable the launcher + activity if you want and enable it again from the main activity if required. + \n\nThe launcher activity is an alias for the current main activity of the app which can + still be opened after disabling the launcher activity. The main activity can be opened + from `&TERMUX_APP_NAME; app settings` -> `&TERMUX_TASKER_APP_NAME;` -> `Open App` if the + option has been implemented in your installed &TERMUX_APP_NAME; app version. Otherwise, + running the `am start "%1$s/%2$s"` command in the &TERMUX_APP_NAME; app should open it. + \n\nNote that on some devices the APIs may not function properly if the launcher activity is + disabled. + + + - Termux - Executable (file in ~/.termux/tasker/ or absolute path to executable) - Arguments - Working directory path - Execute in a terminal session - Executable Absolute Path: \"%1$s\" - Working Directory Absolute Path: \"%1$s\" - - Visit https://github.com/termux/termux-tasker for more info on plugin usage. - - (unknown) - Executable required. - The executable is null or empty. - The directory is null or empty. - Regular file not found at path. - Directory not found at path. - File at path is not readable. - File at path is not an executable. - Directory at path is not readable. - Directory at path is not executable. - Non-regular file found at path. - Non-directory file found at path. - Failed to create directory at path: \"%1$s\" - Failed to create directory at path: \"%1$s\"\nException: %2$s - (variable detected) - No ~/.termux/tasker/ directory - You need to create a ~/.termux/tasker/ directory containing scripts to be executed. - The bundle is null. - - Log Level - "Off" - "Normal" - "Debug" - "Verbose" - "*Unknown*" - Logcat log level set to \"%1$s\" for the plugin app and all plugin actions - - The Termux app is not installed or is disabled. It is required by the Termux:Tasker plugin app to run plugin commands." - The Termux app $PREFIX directory is not accessible by the Termux:Tasker plugin app. This may be because you have not installed or setup - Termux app or Termux app and Termux:Tasker app both have different APK signatures because you have managed to install both apps from different sources. - It may also be because termux $PREFIX directory \"/data/data/com.termux/files/usr\" does not exist or does not have read or execute permissions." - The %1$s App (%2$s) has not been granted the \"%3$s\" permission which is required to run the plugin action." - Absolute paths for executables outside ~/.termux/tasker/ directory require allow-external-apps property to be set to \"true\" in ~/.termux/termux.properties file. - - %1$s %2$s - %1$s %2$s\n\n\u2713 Terminal Session + &TERMUX_APP_NAME; + Executable (file in &TERMUX_TASKER_SCRIPTS_DIR_PATH_SHORT; or absolute path to executable) + Arguments + Working directory path + Stdin + Terminal Session Action + Custom Log Level + Execute in a terminal session + Wait for result for commands (Requires timeout > 0) + + Executable Absolute Path:\n\"%1$s\" + Absolute Path:\n\"%1$s\" + + Executable required. + The executable is null or empty. + Value must be in between %1$d and %2$d. + + (unknown) + The bundle is null. + (variable detected) + + The %1$s App (%2$s) has not been granted the \"%3$s\" permission which is required to run the plugin action." + Absolute paths for executables outside &TERMUX_TASKER_SCRIPTS_DIR_PATH_SHORT; directory require allow-external-apps property to be set to \"true\" in &TERMUX_PROPERTIES_PRIMARY_PATH_SHORT; file. + Failed to generate plugin bundle + Failed to get version code for %1$s + + %1$s%2$s + Working Directory %1$s + Stdin %1$s + Custom Log Level %1$s + Session Action %1$s + Terminal Session %1$s + Wait For Result %1$s %1$s%2$s%3$s diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 785f21f..d781ec5 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,12 +1,4 @@ - - - diff --git a/app/dev_keystore.jks b/app/testkey_untrusted.jks similarity index 100% rename from app/dev_keystore.jks rename to app/testkey_untrusted.jks diff --git a/build.gradle b/build.gradle index 6b72114..f9f4f8d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,18 +1,20 @@ buildscript { repositories { - jcenter() google() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.1' + classpath "com.android.tools.build:gradle:8.7.3" } } allprojects { repositories { - jcenter() google() + mavenCentral() + //mavenLocal() + maven { url "https://jitpack.io" } } } diff --git a/fastlane/metadata/android/en-US/changelogs/6.txt b/fastlane/metadata/android/en-US/changelogs/6.txt new file mode 100644 index 0000000..8081e53 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/6.txt @@ -0,0 +1 @@ +Check changelog at https://github.com/termux/termux-tasker/releases/tag/v0.6.0. diff --git a/fastlane/metadata/android/en-US/changelogs/7.txt b/fastlane/metadata/android/en-US/changelogs/7.txt new file mode 100644 index 0000000..50f28ec --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/7.txt @@ -0,0 +1 @@ +Check changelog at https://github.com/termux/termux-tasker/releases/tag/v0.7.0. diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt new file mode 100644 index 0000000..1d09a0e --- /dev/null +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -0,0 +1,6 @@ +This plugin for https://f-droid.org/packages/com.termux provides a way to run Termux commands from Tasker and other plugin host apps. Check https://github.com/termux/termux-tasker for usage details. + +* Supports running commands in background and in termux terminal session. +* Supports getting back result of commands like stdout, stderr and exit code in Tasker. +* Supports calling scripts/executables defined in ~/.termux/tasker or other directories like in $PREFIX. +* Supports defining limited size scripts inside the plugin configuration and passing them as stdin to shells like bash and python instead of having to create physical files. diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png new file mode 100644 index 0000000..e3ce8bc Binary files /dev/null and b/fastlane/metadata/android/en-US/images/icon.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg new file mode 100755 index 0000000..5ecc741 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg new file mode 100755 index 0000000..d9a0c38 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg new file mode 100755 index 0000000..96f5d28 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.jpg new file mode 100755 index 0000000..95c8519 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.jpg new file mode 100755 index 0000000..fb1a031 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.jpg new file mode 100755 index 0000000..431f8c9 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.jpg differ diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt new file mode 100644 index 0000000..3ebe519 --- /dev/null +++ b/fastlane/metadata/android/en-US/short_description.txt @@ -0,0 +1 @@ +A Termux plugin app allowing Termux commands to be executed from Tasker and other plugin host apps. diff --git a/gradle.properties b/gradle.properties index 9e6fce1..2a17082 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,7 +9,6 @@ # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -android.enableJetifier=true android.useAndroidX=true org.gradle.jvmargs=-Xmx1536m @@ -17,3 +16,7 @@ org.gradle.jvmargs=-Xmx1536m # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true + +minSdkVersion=21 +targetSdkVersion=28 +compileSdkVersion=35 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 41864b2..2fa91c5 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Tue Nov 03 00:12:00 PKT 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip