├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── github_action_build.yml │ └── github_release_build.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── app ├── build.gradle ├── proguard-rules.pro ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── termux │ │ │ └── window │ │ │ ├── FloatingBubbleManager.java │ │ │ ├── TermuxFloatActivity.java │ │ │ ├── TermuxFloatApplication.java │ │ │ ├── TermuxFloatPermissionActivity.java │ │ │ ├── TermuxFloatService.java │ │ │ ├── TermuxFloatSessionClient.java │ │ │ ├── TermuxFloatView.java │ │ │ ├── TermuxFloatViewClient.java │ │ │ └── settings │ │ │ └── properties │ │ │ └── TermuxFloatAppSharedProperties.java │ │ └── res │ │ ├── drawable-anydpi-v26 │ │ └── ic_launcher.xml │ │ ├── drawable │ │ ├── floating_window_background.xml │ │ ├── floating_window_background_resize.xml │ │ ├── ic_exit_icon.xml │ │ ├── ic_foreground.xml │ │ ├── ic_launcher.xml │ │ ├── ic_minimize_icon.xml │ │ └── round_button_with_outline.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ └── activity_permission.xml │ │ ├── mipmap-hdpi │ │ └── ic_service_notification.png │ │ ├── mipmap-xhdpi │ │ └── ic_service_notification.png │ │ ├── mipmap-xxhdpi │ │ └── ic_service_notification.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_service_notification.png │ │ └── values │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml └── testkey_untrusted.jks ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve Termux:Float application 4 | 5 | --- 6 | 7 | 13 | 14 | **Problem description** 15 | 19 | 20 | **Steps to reproduce** 21 | 25 | 26 | **Expected behavior** 27 | 30 | 31 | **Additional information** 32 | 33 | * Termux application version: 34 | * Android OS version: 35 | * Device model: 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a new feature for Termux:Float application 4 | 5 | --- 6 | 7 | 13 | 14 | **Feature description** 15 | 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: daily 7 | -------------------------------------------------------------------------------- /.github/workflows/github_action_build.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Action Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | schedule: 11 | - cron: "15 0 1 */2 *" 12 | workflow_dispatch: 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Clone repository 19 | uses: actions/checkout@v4 20 | 21 | - name: Build 22 | shell: bash {0} 23 | run: | 24 | exit_on_error() { echo "$1"; exit 1; } 25 | 26 | echo "Setting vars" 27 | 28 | if [ "$GITHUB_EVENT_NAME" == "pull_request" ]; then 29 | GITHUB_SHA="${{ github.event.pull_request.head.sha }}" # Do not use last merge commit set in GITHUB_SHA 30 | fi 31 | 32 | # Set RELEASE_VERSION_NAME to "+" 33 | CURRENT_VERSION_NAME_REGEX='\s+versionName "([^"]+)"$' 34 | CURRENT_VERSION_NAME="$(grep -m 1 -E "$CURRENT_VERSION_NAME_REGEX" ./app/build.gradle | sed -r "s/$CURRENT_VERSION_NAME_REGEX/\1/")" 35 | RELEASE_VERSION_NAME="v$CURRENT_VERSION_NAME+${GITHUB_SHA:0:7}" # The "+" is necessary so that versioning precedence is not affected 36 | 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 37 | 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." 38 | fi 39 | 40 | APK_DIR_PATH="./app/build/outputs/apk/debug" 41 | APK_VERSION_TAG="$RELEASE_VERSION_NAME.github.debug" # Note the ".", GITHUB_SHA will already have "+" before it 42 | APK_BASENAME_PREFIX="termux-float-app_$APK_VERSION_TAG" 43 | 44 | # Used by upload step later 45 | echo "APK_DIR_PATH=$APK_DIR_PATH" >> $GITHUB_ENV 46 | echo "APK_VERSION_TAG=$APK_VERSION_TAG" >> $GITHUB_ENV 47 | echo "APK_BASENAME_PREFIX=$APK_BASENAME_PREFIX" >> $GITHUB_ENV 48 | 49 | echo "Building APK file for '$RELEASE_VERSION_NAME' release with '$APK_VERSION_TAG' tag" 50 | export TERMUX_FLOAT_APP__BUILD__APP_VERSION_NAME="${RELEASE_VERSION_NAME/v/}" # Used by app/build.gradle 51 | export TERMUX_FLOAT_APP__BUILD__APK_VERSION_TAG="$APK_VERSION_TAG" # Used by app/build.gradle 52 | if ! ./gradlew assembleDebug; then 53 | exit_on_error "Build failed for '$RELEASE_VERSION_NAME' release with '$APK_VERSION_TAG' tag." 54 | fi 55 | 56 | echo "Validating APK file" 57 | if ! test -f "$APK_DIR_PATH/${APK_BASENAME_PREFIX}.apk"; then 58 | files_found="$(ls "$APK_DIR_PATH")" 59 | exit_on_error "Failed to find built APK file at '$APK_DIR_PATH/${APK_BASENAME_PREFIX}.apk'. Files found: "$'\n'"$files_found" 60 | fi 61 | 62 | echo "Generating checksums-sha256.txt file" 63 | if ! (cd "$APK_DIR_PATH"; sha256sum "${APK_BASENAME_PREFIX}.apk" > checksums-sha256.txt); then 64 | exit_on_error "Generate checksums-sha256.txt file failed for '$RELEASE_VERSION_NAME' release." 65 | fi 66 | echo "checksums-sha256.txt:"$'\n```\n'"$(cat "$APK_DIR_PATH/checksums-sha256.txt")"$'\n```' 67 | 68 | - name: Upload files to action 69 | uses: actions/upload-artifact@v4 70 | with: 71 | name: ${{ env.APK_BASENAME_PREFIX }} 72 | path: | 73 | ${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}.apk 74 | ${{ env.APK_DIR_PATH }}/checksums-sha256.txt 75 | ${{ env.APK_DIR_PATH }}/output-metadata.json 76 | -------------------------------------------------------------------------------- /.github/workflows/github_release_build.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Release Build 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | env: 12 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 13 | steps: 14 | - name: Clone repository 15 | uses: actions/checkout@v4 16 | with: 17 | ref: ${{ env.GITHUB_REF }} 18 | 19 | - name: Build and upload files to release 20 | shell: bash {0} 21 | run: | 22 | exit_on_error() { 23 | echo "$1" 24 | echo "Deleting '$RELEASE_VERSION_NAME' release and '$GITHUB_REF' tag" 25 | hub release delete "$RELEASE_VERSION_NAME" 26 | git push --delete origin "$GITHUB_REF" 27 | exit 1 28 | } 29 | 30 | echo "Setting vars" 31 | RELEASE_VERSION_NAME="${GITHUB_REF/refs\/tags\//}" 32 | 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 33 | 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." 34 | fi 35 | 36 | APK_DIR_PATH="./app/build/outputs/apk/debug" 37 | APK_VERSION_TAG="$RELEASE_VERSION_NAME+github.debug" 38 | APK_BASENAME_PREFIX="termux-float-app_$APK_VERSION_TAG" 39 | 40 | echo "Building APK file for '$RELEASE_VERSION_NAME' release with '$APK_VERSION_TAG' tag" 41 | export TERMUX_FLOAT_APP__BUILD__APK_VERSION_TAG="$APK_VERSION_TAG" # Used by app/build.gradle 42 | if ! ./gradlew assembleDebug; then 43 | exit_on_error "Build failed for '$RELEASE_VERSION_NAME' release with '$APK_VERSION_TAG' tag." 44 | fi 45 | 46 | echo "Validating APK file" 47 | if ! test -f "$APK_DIR_PATH/${APK_BASENAME_PREFIX}.apk"; then 48 | files_found="$(ls "$APK_DIR_PATH")" 49 | exit_on_error "Failed to find built APK file at '$APK_DIR_PATH/${APK_BASENAME_PREFIX}.apk'. Files found: "$'\n'"$files_found" 50 | fi 51 | 52 | echo "Generating checksums-sha256.txt file" 53 | if ! (cd "$APK_DIR_PATH"; sha256sum "${APK_BASENAME_PREFIX}.apk" > checksums-sha256.txt); then 54 | exit_on_error "Generate checksums-sha256.txt file failed for '$RELEASE_VERSION_NAME' release." 55 | fi 56 | echo "checksums-sha256.txt:"$'\n```\n'"$(cat "$APK_DIR_PATH/checksums-sha256.txt")"$'\n```' 57 | 58 | echo "Uploading files to release" 59 | if ! gh release upload "$RELEASE_VERSION_NAME" \ 60 | "$APK_DIR_PATH/${APK_BASENAME_PREFIX}.apk" \ 61 | "$APK_DIR_PATH/checksums-sha256.txt" \ 62 | ; then 63 | exit_on_error "Upload files to release failed for '$RELEASE_VERSION_NAME' release." 64 | fi 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | release/ 3 | .gradle/ 4 | .idea/ 5 | *.iml 6 | 7 | local.properties 8 | github.properties 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The `termux/termux-float` repository is released under [GPLv3 only](https://www.gnu.org/licenses/gpl-3.0.html) license. 2 | 3 | ### Exceptions 4 | 5 | - [Terminal Emulator for Android](https://github.com/jackpal/Android-Terminal-Emulator) code is used which is released under [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). Check [`terminal-view`](https://github.com/termux/termux-app/tree/master/terminal-view) and [`terminal-emulator`](https://github.com/termux/termux-app/tree/master/terminal-emulator) libraries. 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Termux:Float 2 | 3 | [![Build status](https://github.com/termux/termux-float/workflows/Build/badge.svg)](https://github.com/termux/termux-float/actions) 4 | [![Join the chat at https://gitter.im/termux/termux](https://badges.gitter.im/termux/termux.svg)](https://gitter.im/termux/termux) 5 | 6 | A [Termux] plugin app to show the terminal in a floating terminal window. 7 | ## 8 | 9 | 10 | 11 | ### Contents 12 | - [Installation](#Installation) 13 | - [Terminal and App Settings](#Terminal-and-App-Settings) 14 | - [Debugging](#Debugging) 15 | - [Worthy Of Note](#Worthy-Of-Note) 16 | - [For Maintainers and Contributors](#For-Maintainers-and-Contributors) 17 | - [Forking](#Forking) 18 | ## 19 | 20 | 21 | 22 | ### Installation 23 | 24 | Latest version is `v0.15.0`. 25 | 26 | **Check [`termux-app` Installation](https://github.com/termux/termux-app#Installation) for details before reading forward.** 27 | 28 | ### F-Droid 29 | 30 | `Termux:Float` application can be obtained from `F-Droid` from [here](https://f-droid.org/en/packages/com.termux.window). 31 | 32 | You **do not** need to download the `F-Droid` app (via the `Download F-Droid` link) to install `Termux:Float`. You can download the `Termux:Float` APK directly from the site by clicking the `Download APK` link at the bottom of each version section. 33 | 34 | 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.window.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. 35 | 36 | 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. 37 | 38 | ### Github 39 | 40 | `Termux:Float` application can be obtained on `Github` either from [`Github Releases`](https://github.com/termux/termux-float/releases) for version `>= 0.15.0` or from [`Github Actions`](https://github.com/termux/termux-float/actions/workflows/github_action_build.yml?query=branch%3Amaster+event%3Apush). 41 | 42 | 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. 43 | 44 | 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. 45 | 46 | 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. 47 | 48 | ### Google Play Store **(Deprecated)** 49 | 50 | **Termux and its plugins are no longer updated on [Google Play Store](https://play.google.com/store/apps/details?id=com.termux.window) 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. 51 | ## 52 | 53 | 54 | 55 | ### Terminal and App Settings 56 | 57 | The `Termux:Float` app supports defining various settings in `~/.termux/termux.float.properties` file like the `Termux` app does in `~/.termux/termux.properties` file for version `>= 0.15.0`. Currently, only the following properties are supported: `enforce-char-based-input`, `ctrl-space-workaround`, `bell-character`, `terminal-cursor-style`, `terminal-transcript-rows`, `back-key`, `default-working-directory`, `volume-keys`. Check [Terminal Settings](https://wiki.termux.com/wiki/Terminal_Settings) for more info. The `~/` is a shortcut for the Termux home directory `/data/data/com.termux/files/home/` and can also be referred by the `$HOME` shell environment variable. 58 | 59 | You can create/edit it by running the below commands to open the `nano` text editor in the terminal. Press `Ctrl+o` and then `Enter` to save and `Ctrl+x` to exit. You can also edit it with a [SAF file browser](https://github.com/termux/termux-tasker#Creating-And-Modifying-Scripts) after creating it. 60 | 61 | ``` 62 | mkdir -p ~/.termux 63 | nano ~/.termux/termux.float.properties 64 | ``` 65 | 66 | ## 67 | 68 | 69 | 70 | ### Debugging 71 | 72 | You can help debug problems by setting appropriate `logcat` `Log Level` in `Termux` app settings -> `Termux:Float` -> `Debugging` -> `Log Level` (Requires `Termux` app version `>= 0.118.0`). The `Log Level` defaults to `Normal` and log level `Verbose` currently logs additional information. Its best to revert log level to `Normal` after you have finished debugging since private data may otherwise be passed to `logcat` during normal operation and moreover, additional logging increases execution time. 73 | 74 | Once log levels have been set, you can run the `logcat` command in `Termux` or `Termux:Float` 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). 75 | 76 | ##### Log Levels 77 | - `Off` - Log nothing 78 | - `Normal` - Start logging error, warn and info messages and stacktraces 79 | - `Debug` - Start logging debug messages 80 | - `Verbose` - Start logging verbose messages 81 | ## 82 | 83 | 84 | 85 | ## For Maintainers and Contributors 86 | 87 | Check [For Maintainers and Contributors](https://github.com/termux/termux-app#For-Maintainers-and-Contributors) section of `termux/termux-app` `README` for details. 88 | ## 89 | 90 | 91 | 92 | ## Forking 93 | 94 | Check [Forking](https://github.com/termux/termux-app#Forking) section of `termux/termux-app` `README` for details. 95 | ## 96 | 97 | 98 | 99 | [Termux]: https://termux.dev 100 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | namespace "com.termux.window" 5 | 6 | compileSdk project.properties.compileSdkVersion.toInteger() 7 | def appVersionName = System.getenv("TERMUX_FLOAT_APP__BUILD__APP_VERSION_NAME") ?: "" 8 | def apkVersionTag = System.getenv("TERMUX_FLOAT_APP__BUILD__APK_VERSION_TAG") ?: "" 9 | 10 | defaultConfig { 11 | applicationId "com.termux.window" 12 | minSdk project.properties.minSdkVersion.toInteger() 13 | targetSdk project.properties.targetSdkVersion.toInteger() 14 | versionCode 1000 15 | versionName "0.16.0" 16 | 17 | if (appVersionName) versionName = appVersionName 18 | validateVersionName(versionName) 19 | 20 | manifestPlaceholders.TERMUX_PACKAGE_NAME = "com.termux" 21 | manifestPlaceholders.TERMUX_APP_NAME = "Termux" 22 | manifestPlaceholders.TERMUX_FLOAT_APP_NAME = "Termux:Float" 23 | } 24 | 25 | signingConfigs { 26 | debug { 27 | storeFile file('testkey_untrusted.jks') 28 | keyAlias 'alias' 29 | storePassword 'xrj45yWGLbsO7W0v' 30 | keyPassword 'xrj45yWGLbsO7W0v' 31 | } 32 | } 33 | 34 | buildTypes { 35 | release { 36 | minifyEnabled true 37 | shrinkResources false // Reproducible builds 38 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 39 | } 40 | 41 | debug { 42 | signingConfig signingConfigs.debug 43 | zipAlignEnabled true 44 | } 45 | } 46 | 47 | compileOptions { 48 | // Flag to enable support for the new language APIs 49 | coreLibraryDesugaringEnabled true 50 | 51 | sourceCompatibility JavaVersion.VERSION_11 52 | targetCompatibility JavaVersion.VERSION_11 53 | } 54 | 55 | applicationVariants.all { variant -> 56 | variant.outputs.all { output -> 57 | outputFileName = new File("termux-float-app_" + 58 | (apkVersionTag ? apkVersionTag : "v" + versionName + "+" + variant.buildType.name) + ".apk") 59 | } 60 | } 61 | 62 | packagingOptions { 63 | // Remove terminal-shared JNI libs added via termux-shared dependency 64 | exclude "lib/*/liblocal-socket.so" 65 | } 66 | } 67 | 68 | dependencies { 69 | coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.1.5" 70 | 71 | testImplementation "junit:junit:4.13.2" 72 | 73 | implementation "androidx.annotation:annotation:1.9.1" 74 | 75 | implementation "com.termux.termux-app:termux-shared:da3a0ac4e2" 76 | implementation "com.termux.termux-app:terminal-view:da3a0ac4e2" 77 | 78 | // Use if below libraries are published locally by termux-app with `./gradlew publishReleasePublicationToMavenLocal` and used with `mavenLocal()`. 79 | // If updates are done, republish there and sync project with gradle files here 80 | // https://github.com/termux/termux-app/wiki/Termux-Libraries 81 | //implementation "com.termux:termux-shared:0.118.0" 82 | //implementation "com.termux:terminal-view:0.118.0" 83 | 84 | implementation "com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava" 85 | } 86 | 87 | task versionName { 88 | doLast { 89 | print android.defaultConfig.versionName 90 | } 91 | } 92 | 93 | def validateVersionName(String versionName) { 94 | // https://semver.org/spec/v2.0.0.html#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string 95 | // ^(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-]+)*))?$ 96 | 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)) 97 | 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.") 98 | } 99 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/fornwall/lib/android-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | -dontobfuscate 11 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 20 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/termux/window/FloatingBubbleManager.java: -------------------------------------------------------------------------------- 1 | package com.termux.window; 2 | 3 | import android.graphics.Outline; 4 | import android.graphics.drawable.Drawable; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.view.ViewOutlineProvider; 8 | import android.view.WindowManager; 9 | 10 | import com.termux.shared.view.ViewUtils; 11 | import com.termux.view.TerminalView; 12 | 13 | /** 14 | * Handles displaying our TermuxFloatView as a collapsed bubble and restoring back 15 | * to its original display. 16 | */ 17 | public class FloatingBubbleManager { 18 | private static final int DEFAULT_BUBBLE_SIZE_DP = 56; 19 | 20 | private TermuxFloatView mTermuxFloatView; 21 | private final int BUBBLE_SIZE_PX; 22 | 23 | private boolean mIsMinimized; 24 | 25 | // preserve original layout values so we can restore to normal window 26 | // from our bubble 27 | private int mOriginalLayoutWidth; 28 | private int mOriginalLayoutHeight; 29 | private boolean mDidCaptureOriginalValues; 30 | private Drawable mOriginalTerminalViewBackground; 31 | private Drawable mOriginalFloatViewBackground; 32 | 33 | public FloatingBubbleManager(TermuxFloatView termuxFloatView) { 34 | mTermuxFloatView = termuxFloatView; 35 | BUBBLE_SIZE_PX = (int) ViewUtils.dpToPx(mTermuxFloatView.getContext(), DEFAULT_BUBBLE_SIZE_DP); 36 | } 37 | 38 | public void toggleBubble() { 39 | if (isMinimized()) { 40 | displayAsFloatingWindow(); 41 | } else { 42 | displayAsFloatingBubble(); 43 | } 44 | } 45 | 46 | public void updateLongPressBackgroundResource(boolean isInLongPressState) { 47 | if (isMinimized()) { 48 | return; 49 | } 50 | mTermuxFloatView.setBackgroundResource(isInLongPressState ? R.drawable.floating_window_background_resize : R.drawable.floating_window_background); 51 | } 52 | 53 | public void displayAsFloatingBubble() { 54 | captureOriginalLayoutValues(); 55 | 56 | WindowManager.LayoutParams layoutParams = getLayoutParams(); 57 | 58 | layoutParams.width = BUBBLE_SIZE_PX; 59 | layoutParams.height = BUBBLE_SIZE_PX; 60 | 61 | TerminalView terminalView = getTerminalView(); 62 | final int strokeWidth = (int) terminalView.getResources().getDimension(R.dimen.bubble_outline_stroke_width); 63 | terminalView.setOutlineProvider(new ViewOutlineProvider() { 64 | @SuppressWarnings("SuspiciousNameCombination") 65 | @Override 66 | public void getOutline(View view, Outline outline) { 67 | // shrink TerminalView clipping a bit so it doesn't cut off our bubble outline 68 | outline.setOval(strokeWidth, strokeWidth, view.getWidth() - strokeWidth, view.getHeight() - strokeWidth); 69 | } 70 | }); 71 | terminalView.setClipToOutline(true); 72 | 73 | TermuxFloatView termuxFloatView = getTermuxFloatView(); 74 | termuxFloatView.setBackgroundResource(R.drawable.round_button_with_outline); 75 | termuxFloatView.setClipToOutline(true); 76 | termuxFloatView.hideTouchKeyboard(); 77 | termuxFloatView.changeFocus(false); 78 | 79 | ViewGroup windowControls = termuxFloatView.findViewById(R.id.window_controls); 80 | windowControls.setVisibility(View.GONE); 81 | 82 | getWindowManager().updateViewLayout(termuxFloatView, layoutParams); 83 | mIsMinimized = true; 84 | } 85 | 86 | public void displayAsFloatingWindow() { 87 | WindowManager.LayoutParams layoutParams = getLayoutParams(); 88 | 89 | // restore back to previous values 90 | layoutParams.width = mOriginalLayoutWidth; 91 | layoutParams.height = mOriginalLayoutHeight; 92 | 93 | TerminalView terminalView = getTerminalView(); 94 | terminalView.setBackground(mOriginalTerminalViewBackground); 95 | terminalView.setOutlineProvider(null); 96 | terminalView.setClipToOutline(false); 97 | 98 | TermuxFloatView termuxFloatView = getTermuxFloatView(); 99 | termuxFloatView.setBackground(mOriginalFloatViewBackground); 100 | termuxFloatView.setClipToOutline(false); 101 | 102 | ViewGroup windowControls = termuxFloatView.findViewById(R.id.window_controls); 103 | windowControls.setVisibility(View.VISIBLE); 104 | 105 | getWindowManager().updateViewLayout(termuxFloatView, layoutParams); 106 | mIsMinimized = false; 107 | 108 | // clear so we can capture proper values on next minimize 109 | mDidCaptureOriginalValues = false; 110 | } 111 | 112 | public boolean isMinimized() { 113 | return mIsMinimized; 114 | } 115 | 116 | private void captureOriginalLayoutValues() { 117 | if (!mDidCaptureOriginalValues) { 118 | WindowManager.LayoutParams layoutParams = getLayoutParams(); 119 | mOriginalLayoutWidth = layoutParams.width; 120 | mOriginalLayoutHeight = layoutParams.height; 121 | 122 | mOriginalTerminalViewBackground = getTerminalView().getBackground(); 123 | mOriginalFloatViewBackground = getTermuxFloatView().getBackground(); 124 | mDidCaptureOriginalValues = true; 125 | } 126 | } 127 | 128 | public void cleanup() { 129 | mTermuxFloatView = null; 130 | mOriginalFloatViewBackground = null; 131 | mOriginalTerminalViewBackground = null; 132 | } 133 | 134 | private TermuxFloatView getTermuxFloatView() { 135 | return mTermuxFloatView; 136 | } 137 | 138 | private TerminalView getTerminalView() { 139 | return mTermuxFloatView.getTerminalView(); 140 | } 141 | 142 | private WindowManager getWindowManager() { 143 | return mTermuxFloatView.mWindowManager; 144 | } 145 | 146 | private WindowManager.LayoutParams getLayoutParams() { 147 | return (WindowManager.LayoutParams) mTermuxFloatView.getLayoutParams(); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /app/src/main/java/com/termux/window/TermuxFloatActivity.java: -------------------------------------------------------------------------------- 1 | package com.termux.window; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | 6 | /** 7 | * Simple activity which immediately launches {@link TermuxFloatService} and exits. 8 | */ 9 | public class TermuxFloatActivity extends Activity { 10 | 11 | @Override 12 | protected void onResume() { 13 | super.onResume(); 14 | 15 | // Set log level for the app 16 | TermuxFloatApplication.setLogConfig(this, false); 17 | 18 | startService(new Intent(this, TermuxFloatService.class)); 19 | finish(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/termux/window/TermuxFloatApplication.java: -------------------------------------------------------------------------------- 1 | package com.termux.window; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import android.util.Log; 6 | 7 | import com.termux.shared.logger.Logger; 8 | import com.termux.shared.termux.TermuxConstants; 9 | import com.termux.shared.termux.crash.TermuxCrashUtils; 10 | import com.termux.shared.termux.settings.preferences.TermuxFloatAppSharedPreferences; 11 | 12 | public class TermuxFloatApplication extends Application { 13 | 14 | public static final String LOG_TAG = "TermuxFloatApplication"; 15 | 16 | public void onCreate() { 17 | super.onCreate(); 18 | 19 | Log.i(LOG_TAG, "AppInit"); 20 | 21 | Context context = getApplicationContext(); 22 | 23 | // Set crash handler for the app 24 | TermuxCrashUtils.setCrashHandler(context); 25 | 26 | // Set log config for the app 27 | setLogConfig(context, true); 28 | } 29 | 30 | public static void setLogConfig(Context context, boolean commitToFile) { 31 | Logger.setDefaultLogTag(TermuxConstants.TERMUX_FLOAT_APP_NAME.replaceAll("[: ]", "")); 32 | 33 | // Load the log level from shared preferences and set it to the {@link Logger.CURRENT_LOG_LEVEL} 34 | TermuxFloatAppSharedPreferences preferences = TermuxFloatAppSharedPreferences.build(context); 35 | if (preferences == null) return; 36 | preferences.setLogLevel(null, preferences.getLogLevel(true), commitToFile); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/termux/window/TermuxFloatPermissionActivity.java: -------------------------------------------------------------------------------- 1 | package com.termux.window; 2 | 3 | import android.annotation.TargetApi; 4 | import android.app.Activity; 5 | import android.content.Intent; 6 | import android.net.Uri; 7 | import android.os.Bundle; 8 | import android.provider.Settings; 9 | import android.view.View; 10 | 11 | @TargetApi(23) 12 | public class TermuxFloatPermissionActivity extends Activity { 13 | 14 | public static int OVERLAY_PERMISSION_REQ_CODE = 1234; 15 | 16 | @Override 17 | protected void onCreate(Bundle savedInstanceState) { 18 | super.onCreate(savedInstanceState); 19 | setContentView(R.layout.activity_permission); 20 | } 21 | 22 | public void onOkButton(View view) { 23 | Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())); 24 | startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE); 25 | } 26 | 27 | @Override 28 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 29 | if (requestCode == OVERLAY_PERMISSION_REQ_CODE) { 30 | startService(new Intent(this, TermuxFloatService.class)); 31 | finish(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/termux/window/TermuxFloatService.java: -------------------------------------------------------------------------------- 1 | package com.termux.window; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.Notification; 5 | import android.app.NotificationManager; 6 | import android.app.PendingIntent; 7 | import android.app.Service; 8 | import android.content.Context; 9 | import android.content.Intent; 10 | import android.content.res.Resources; 11 | import android.os.Build; 12 | import android.os.IBinder; 13 | 14 | import androidx.annotation.Nullable; 15 | import android.view.LayoutInflater; 16 | import android.view.View; 17 | 18 | import com.termux.shared.data.IntentUtils; 19 | import com.termux.shared.logger.Logger; 20 | import com.termux.shared.notification.NotificationUtils; 21 | import com.termux.shared.shell.command.ExecutionCommand; 22 | import com.termux.shared.termux.TermuxConstants; 23 | import com.termux.shared.termux.TermuxConstants.TERMUX_FLOAT_APP.TERMUX_FLOAT_SERVICE; 24 | import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment; 25 | import com.termux.shared.termux.shell.command.runner.terminal.TermuxSession; 26 | import com.termux.terminal.TerminalSession; 27 | 28 | public class TermuxFloatService extends Service { 29 | 30 | private TermuxFloatView mFloatingWindow; 31 | 32 | private TermuxSession mSession; 33 | 34 | private boolean mVisibleWindow = true; 35 | 36 | private static final String LOG_TAG = "TermuxFloatService"; 37 | 38 | @Override 39 | public IBinder onBind(Intent intent) { 40 | return null; 41 | } 42 | 43 | @Override 44 | public void onCreate() { 45 | runStartForeground(); 46 | TermuxFloatApplication.setLogConfig(this, false); 47 | Logger.logVerbose(LOG_TAG, "onCreate"); 48 | } 49 | 50 | @Override 51 | public int onStartCommand(Intent intent, int flags, int startId) { 52 | Logger.logDebug(LOG_TAG, "onStartCommand"); 53 | 54 | // Run again in case service is already started and onCreate() is not called 55 | runStartForeground(); 56 | 57 | if (mFloatingWindow == null && !initializeFloatView()) 58 | return Service.START_NOT_STICKY; 59 | 60 | String action = null; 61 | if (intent != null) { 62 | Logger.logVerboseExtended(LOG_TAG, "Received intent:\n" + IntentUtils.getIntentString(intent)); 63 | action = intent.getAction(); 64 | } 65 | 66 | if (action != null) { 67 | switch (action) { 68 | case TERMUX_FLOAT_SERVICE.ACTION_STOP_SERVICE: 69 | actionStopService(); 70 | break; 71 | case TERMUX_FLOAT_SERVICE.ACTION_SHOW: 72 | setVisible(true); 73 | break; 74 | case TERMUX_FLOAT_SERVICE.ACTION_HIDE: 75 | setVisible(false); 76 | break; 77 | default: 78 | Logger.logError(LOG_TAG, "Invalid action: \"" + action + "\""); 79 | break; 80 | } 81 | } else if (!mVisibleWindow) { 82 | // Show window if hidden when launched through launcher icon. 83 | setVisible(true); 84 | } 85 | 86 | return Service.START_NOT_STICKY; 87 | 88 | } 89 | 90 | @Override 91 | public void onDestroy() { 92 | super.onDestroy(); 93 | Logger.logVerbose(LOG_TAG, "onDestroy"); 94 | 95 | if (mFloatingWindow != null) 96 | mFloatingWindow.closeFloatingWindow(); 97 | 98 | runStopForeground(); 99 | } 100 | /** Request to stop service. */ 101 | public void requestStopService() { 102 | Logger.logDebug(LOG_TAG, "Requesting to stop service"); 103 | runStopForeground(); 104 | stopSelf(); 105 | } 106 | 107 | /** Process action to stop service. */ 108 | private void actionStopService() { 109 | if (mSession != null) 110 | mSession.killIfExecuting(this, false); 111 | requestStopService(); 112 | } 113 | 114 | /** Make service run in foreground mode. */ 115 | private void runStartForeground() { 116 | setupNotificationChannel(); 117 | startForeground(TermuxConstants.TERMUX_FLOAT_APP_NOTIFICATION_ID, buildNotification()); 118 | } 119 | 120 | /** Make service leave foreground mode. */ 121 | private void runStopForeground() { 122 | stopForeground(true); 123 | } 124 | 125 | 126 | 127 | private void setupNotificationChannel() { 128 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return; 129 | 130 | NotificationUtils.setupNotificationChannel(this, TermuxConstants.TERMUX_FLOAT_APP_NOTIFICATION_CHANNEL_ID, 131 | TermuxConstants.TERMUX_FLOAT_APP_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW); 132 | } 133 | 134 | private Notification buildNotification() { 135 | final Resources res = getResources(); 136 | 137 | final String notificationText = res.getString(mVisibleWindow ? R.string.notification_message_visible : R.string.notification_message_hidden); 138 | 139 | final String intentAction = mVisibleWindow ? TERMUX_FLOAT_SERVICE.ACTION_HIDE : TERMUX_FLOAT_SERVICE.ACTION_SHOW; 140 | Intent notificationIntent = new Intent(this, TermuxFloatService.class).setAction(intentAction); 141 | PendingIntent contentIntent = PendingIntent.getService(this, 0, notificationIntent, 0); 142 | 143 | // Build the notification 144 | Notification.Builder builder = NotificationUtils.geNotificationBuilder(this, 145 | TermuxConstants.TERMUX_FLOAT_APP_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_LOW, 146 | TermuxConstants.TERMUX_FLOAT_APP_NAME, notificationText, null, 147 | contentIntent, null, NotificationUtils.NOTIFICATION_MODE_SILENT); 148 | if (builder == null) return null; 149 | 150 | // No need to show a timestamp: 151 | builder.setShowWhen(false); 152 | 153 | // Set notification icon 154 | builder.setSmallIcon(R.mipmap.ic_service_notification); 155 | 156 | // Set background color for small notification icon 157 | builder.setColor(0xFF000000); 158 | 159 | // TermuxSessions are always ongoing 160 | builder.setOngoing(true); 161 | 162 | // Set Exit button action 163 | Intent exitIntent = new Intent(this, TermuxFloatService.class).setAction(TERMUX_FLOAT_SERVICE.ACTION_STOP_SERVICE); 164 | builder.addAction(android.R.drawable.ic_delete, res.getString(R.string.notification_action_exit), PendingIntent.getService(this, 0, exitIntent, 0)); 165 | 166 | return builder.build(); 167 | } 168 | 169 | 170 | 171 | @SuppressLint("InflateParams") 172 | private boolean initializeFloatView() { 173 | boolean floatWindowWasNull = false; 174 | if (mFloatingWindow == null) { 175 | mFloatingWindow = (TermuxFloatView) ((LayoutInflater) 176 | getSystemService(Context.LAYOUT_INFLATER_SERVICE)).inflate(R.layout.activity_main, null); 177 | floatWindowWasNull = true; 178 | } 179 | 180 | mFloatingWindow.initFloatView(this); 181 | 182 | mSession = createTermuxSession( 183 | new ExecutionCommand(0, null, null, null, mFloatingWindow.getProperties().getDefaultWorkingDirectory(), ExecutionCommand.Runner.TERMINAL_SESSION.getName(), false), null); 184 | if (mSession == null) 185 | return false; 186 | mFloatingWindow.getTerminalView().attachSession(mSession.getTerminalSession()); 187 | 188 | try { 189 | mFloatingWindow.launchFloatingWindow(); 190 | } catch (Exception e) { 191 | Logger.logStackTrace(LOG_TAG, e); 192 | // Settings.canDrawOverlays() does not work (always returns false, perhaps due to sharedUserId?). 193 | // So instead we catch the exception and prompt here. 194 | startActivity(new Intent(this, TermuxFloatPermissionActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); 195 | requestStopService(); 196 | return false; 197 | } 198 | 199 | if (floatWindowWasNull) 200 | Logger.showToast(this, getString(R.string.initial_instruction_toast), true); 201 | 202 | return true; 203 | } 204 | 205 | private void setVisible(boolean newVisibility) { 206 | mVisibleWindow = newVisibility; 207 | mFloatingWindow.setVisibility(newVisibility ? View.VISIBLE : View.GONE); 208 | ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).notify(TermuxConstants.TERMUX_FLOAT_APP_NOTIFICATION_ID, buildNotification()); 209 | } 210 | 211 | 212 | 213 | /** Create a {@link TermuxSession}. */ 214 | @Nullable 215 | public synchronized TermuxSession createTermuxSession(ExecutionCommand executionCommand, String sessionName) { 216 | if (executionCommand == null) return null; 217 | 218 | Logger.logDebug(LOG_TAG, "Creating \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession"); 219 | 220 | if (ExecutionCommand.Runner.APP_SHELL.getName().equals(executionCommand.runner)) { 221 | Logger.logDebug(LOG_TAG, "Ignoring a background execution command passed to createTermuxSession()"); 222 | return null; 223 | } 224 | 225 | if (Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE) 226 | Logger.logVerboseExtended(LOG_TAG, executionCommand.toString()); 227 | 228 | executionCommand.shellName = sessionName; 229 | executionCommand.terminalTranscriptRows = mFloatingWindow.getProperties().getTerminalTranscriptRows(); 230 | TermuxSession newTermuxSession = TermuxSession.execute(this, executionCommand, 231 | mFloatingWindow.getTermuxFloatSessionClient(), null, new TermuxShellEnvironment(), 232 | null, executionCommand.isPluginExecutionCommand); 233 | if (newTermuxSession == null) { 234 | Logger.logError(LOG_TAG, "Failed to execute new TermuxSession command for:\n" + executionCommand.getCommandIdAndLabelLogString()); 235 | return null; 236 | } 237 | 238 | // Emulator won't be set at this point so colors won't be set by TermuxFloatSessionClient.checkForFontAndColors() 239 | mFloatingWindow.reloadViewStyling(); 240 | 241 | return newTermuxSession; 242 | } 243 | 244 | public TermuxSession getTermuxSession() { 245 | return mSession; 246 | } 247 | 248 | public TerminalSession getCurrentSession() { 249 | return mSession != null ? mSession.getTerminalSession() : null; 250 | } 251 | 252 | } 253 | -------------------------------------------------------------------------------- /app/src/main/java/com/termux/window/TermuxFloatSessionClient.java: -------------------------------------------------------------------------------- 1 | package com.termux.window; 2 | 3 | import android.content.ClipData; 4 | import android.content.ClipboardManager; 5 | import android.content.Context; 6 | import android.graphics.Typeface; 7 | import android.media.AudioAttributes; 8 | import android.media.SoundPool; 9 | import android.text.TextUtils; 10 | 11 | import com.termux.shared.logger.Logger; 12 | import com.termux.shared.termux.TermuxConstants; 13 | import com.termux.shared.termux.settings.properties.TermuxPropertyConstants; 14 | import com.termux.shared.termux.terminal.TermuxTerminalSessionClientBase; 15 | import com.termux.shared.termux.terminal.io.BellHandler; 16 | import com.termux.terminal.TerminalColors; 17 | import com.termux.terminal.TerminalSession; 18 | import com.termux.terminal.TextStyle; 19 | 20 | import java.io.File; 21 | import java.io.FileInputStream; 22 | import java.io.InputStream; 23 | import java.util.Properties; 24 | 25 | public class TermuxFloatSessionClient extends TermuxTerminalSessionClientBase { 26 | 27 | private final TermuxFloatService mService; 28 | private final TermuxFloatView mView; 29 | 30 | private SoundPool mBellSoundPool; 31 | 32 | private int mBellSoundId; 33 | 34 | private static final String LOG_TAG = "TermuxFloatSessionClient"; 35 | 36 | public TermuxFloatSessionClient(TermuxFloatService service, TermuxFloatView view) { 37 | mService = service; 38 | mView = view; 39 | } 40 | 41 | /** 42 | * Should be called when TermuxFloatView.onAttachedToWindow() is called 43 | */ 44 | public void onAttachedToWindow() { 45 | // Just initialize the mBellSoundPool and load the sound, otherwise bell might not run 46 | // the first time bell key is pressed and play() is called, since sound may not be loaded 47 | // quickly enough before the call to play(). https://stackoverflow.com/questions/35435625 48 | loadBellSoundPool(); 49 | } 50 | 51 | /** 52 | * Should be called when TermuxFloatView.onDetachedFromWindow() is called 53 | */ 54 | public void onDetachedFromWindow() { 55 | // Release mBellSoundPool resources, specially to prevent exceptions like the following to be thrown 56 | // java.util.concurrent.TimeoutException: android.media.SoundPool.finalize() timed out after 10 seconds 57 | // Bell is not played in background anyways 58 | // Related: https://stackoverflow.com/a/28708351/14686958 59 | releaseBellSoundPool(); 60 | } 61 | 62 | /** 63 | * Should be called when TermuxFloatView.onReload() is called 64 | */ 65 | public void onReload() { 66 | checkForFontAndColors(); 67 | } 68 | 69 | 70 | 71 | @Override 72 | public void onTextChanged(TerminalSession changedSession) { 73 | if (!mView.isVisible()) return; 74 | 75 | mView.getTerminalView().onScreenUpdated(); 76 | } 77 | 78 | @Override 79 | public void onSessionFinished(TerminalSession finishedSession) { 80 | mService.requestStopService(); 81 | } 82 | 83 | @Override 84 | public void onCopyTextToClipboard(TerminalSession pastingSession, String text) { 85 | ClipboardManager clipboard = (ClipboardManager) mService.getSystemService(Context.CLIPBOARD_SERVICE); 86 | clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(text))); 87 | } 88 | 89 | @Override 90 | public void onPasteTextFromClipboard(TerminalSession session) { 91 | if (!mView.isVisible()) return; 92 | 93 | ClipboardManager clipboard = (ClipboardManager) mService.getSystemService(Context.CLIPBOARD_SERVICE); 94 | ClipData clipData = clipboard.getPrimaryClip(); 95 | if (clipData != null) { 96 | CharSequence paste = clipData.getItemAt(0).coerceToText(mService); 97 | if (!TextUtils.isEmpty(paste)) mView.getTerminalView().mEmulator.paste(paste.toString()); 98 | } 99 | } 100 | 101 | @Override 102 | public void onBell(TerminalSession session) { 103 | if (!mView.isVisible()) return; 104 | 105 | int bellBehaviour = mView.getProperties().getBellBehaviour(); 106 | if (bellBehaviour == TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_VIBRATE) { 107 | BellHandler.getInstance(mService).doBell(); 108 | } else if (bellBehaviour == TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_BEEP) { 109 | loadBellSoundPool(); 110 | if (mBellSoundPool != null) 111 | mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f); 112 | } else if (bellBehaviour == TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_IGNORE) { 113 | // Ignore the bell character. 114 | } 115 | } 116 | 117 | @Override 118 | public void onColorsChanged(TerminalSession changedSession) { 119 | updateBackgroundColor(); 120 | } 121 | 122 | 123 | @Override 124 | public Integer getTerminalCursorStyle() { 125 | return mView.getProperties().getTerminalCursorStyle(); 126 | } 127 | 128 | 129 | /** Load mBellSoundPool */ 130 | private synchronized void loadBellSoundPool() { 131 | if (mBellSoundPool == null) { 132 | mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes( 133 | new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) 134 | .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build(); 135 | 136 | try { 137 | mBellSoundId = mBellSoundPool.load(mService, com.termux.shared.R.raw.bell, 1); 138 | } catch (Exception e){ 139 | // Catch java.lang.RuntimeException: Unable to resume activity {com.termux/com.termux.app.TermuxActivity}: android.content.res.Resources$NotFoundException: File res/raw/bell.ogg from drawable resource ID 140 | Logger.logStackTraceWithMessage(LOG_TAG, "Failed to load bell sound pool", e); 141 | } 142 | } 143 | } 144 | 145 | /** Release mBellSoundPool resources */ 146 | private synchronized void releaseBellSoundPool() { 147 | if (mBellSoundPool != null) { 148 | mBellSoundPool.release(); 149 | mBellSoundPool = null; 150 | } 151 | } 152 | 153 | 154 | 155 | public void checkForFontAndColors() { 156 | try { 157 | File colorsFile = TermuxConstants.TERMUX_COLOR_PROPERTIES_FILE; 158 | File fontFile = TermuxConstants.TERMUX_FONT_FILE; 159 | 160 | final Properties props = new Properties(); 161 | if (colorsFile.isFile()) { 162 | try (InputStream in = new FileInputStream(colorsFile)) { 163 | props.load(in); 164 | } 165 | } 166 | 167 | TerminalColors.COLOR_SCHEME.updateWith(props); 168 | TerminalSession session = mService.getCurrentSession(); 169 | if (session != null && session.getEmulator() != null) { 170 | session.getEmulator().mColors.reset(); 171 | } 172 | 173 | updateBackgroundColor(); 174 | 175 | final Typeface newTypeface = (fontFile.exists() && fontFile.length() > 0) ? Typeface.createFromFile(fontFile) : Typeface.MONOSPACE; 176 | mView.getTerminalView().setTypeface(newTypeface); 177 | } catch (Exception e) { 178 | Logger.logStackTraceWithMessage(LOG_TAG, "Error in checkForFontAndColors()", e); 179 | } 180 | } 181 | 182 | public void updateBackgroundColor() { 183 | //if (!mView.isVisible()) return; 184 | 185 | TerminalSession session = mService.getCurrentSession(); 186 | if (session != null && session.getEmulator() != null) { 187 | mView.getTerminalView().setBackgroundColor(session.getEmulator().mColors.mCurrentColors[TextStyle.COLOR_INDEX_BACKGROUND]); 188 | } 189 | } 190 | 191 | } 192 | -------------------------------------------------------------------------------- /app/src/main/java/com/termux/window/TermuxFloatView.java: -------------------------------------------------------------------------------- 1 | package com.termux.window; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.graphics.PixelFormat; 7 | import android.graphics.Point; 8 | import android.os.Build; 9 | import android.util.AttributeSet; 10 | import android.view.Gravity; 11 | import android.view.MotionEvent; 12 | import android.view.ScaleGestureDetector; 13 | import android.view.ScaleGestureDetector.OnScaleGestureListener; 14 | import android.view.View; 15 | import android.view.ViewGroup; 16 | import android.view.WindowManager; 17 | import android.widget.Button; 18 | import android.widget.LinearLayout; 19 | 20 | import com.termux.shared.logger.Logger; 21 | import com.termux.shared.termux.TermuxConstants; 22 | import com.termux.shared.termux.settings.preferences.TermuxFloatAppSharedPreferences; 23 | import com.termux.shared.view.KeyboardUtils; 24 | import com.termux.terminal.TerminalSession; 25 | import com.termux.terminal.TerminalSessionClient; 26 | import com.termux.view.TerminalView; 27 | import com.termux.view.TerminalViewClient; 28 | import com.termux.window.settings.properties.TermuxFloatAppSharedProperties; 29 | 30 | public class TermuxFloatView extends LinearLayout { 31 | 32 | public static final float ALPHA_FOCUS = 0.9f; 33 | public static final float ALPHA_NOT_FOCUS = 0.7f; 34 | public static final float ALPHA_MOVING = 0.5f; 35 | 36 | private int DISPLAY_WIDTH, DISPLAY_HEIGHT; 37 | 38 | final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(); 39 | WindowManager mWindowManager; 40 | 41 | private TerminalView mTerminalView; 42 | ViewGroup mWindowControls; 43 | FloatingBubbleManager mFloatingBubbleManager; 44 | 45 | /** 46 | * The {@link TerminalViewClient} interface implementation to allow for communication between 47 | * {@link TerminalView} and {@link TermuxFloatView}. 48 | */ 49 | TermuxFloatViewClient mTermuxFloatViewClient; 50 | 51 | /** 52 | * The {@link TerminalSessionClient} interface implementation to allow for communication between 53 | * {@link TerminalSession} and {@link TermuxFloatService}. 54 | */ 55 | TermuxFloatSessionClient mTermuxFloatSessionClient; 56 | 57 | /** 58 | * Termux Float app shared preferences manager. 59 | */ 60 | private TermuxFloatAppSharedPreferences mPreferences; 61 | 62 | /** 63 | * Termux app shared properties manager, loaded from termux.properties 64 | */ 65 | private TermuxFloatAppSharedProperties mProperties; 66 | 67 | private boolean withFocus = true; 68 | int initialX; 69 | int initialY; 70 | float initialTouchX; 71 | float initialTouchY; 72 | 73 | boolean isInLongPressState; 74 | 75 | final int[] location = new int[2]; 76 | 77 | final int[] windowControlsLocation = new int[2]; 78 | 79 | private static final String LOG_TAG = "TermuxFloatView"; 80 | 81 | final ScaleGestureDetector mScaleDetector = new ScaleGestureDetector(getContext(), new OnScaleGestureListener() { 82 | private static final int MIN_SIZE = 50; 83 | 84 | @Override 85 | public boolean onScaleBegin(ScaleGestureDetector detector) { 86 | return true; 87 | } 88 | 89 | @Override 90 | public boolean onScale(ScaleGestureDetector detector) { 91 | int widthChange = (int) (detector.getCurrentSpanX() - detector.getPreviousSpanX()); 92 | int heightChange = (int) (detector.getCurrentSpanY() - detector.getPreviousSpanY()); 93 | layoutParams.width += widthChange; 94 | layoutParams.height += heightChange; 95 | layoutParams.width = Math.max(MIN_SIZE, layoutParams.width); 96 | layoutParams.height = Math.max(MIN_SIZE, layoutParams.height); 97 | mWindowManager.updateViewLayout(TermuxFloatView.this, layoutParams); 98 | if (mPreferences != null) { 99 | mPreferences.setWindowWidth(layoutParams.width); 100 | mPreferences.setWindowHeight(layoutParams.height); 101 | } 102 | return true; 103 | } 104 | 105 | @Override 106 | public void onScaleEnd(ScaleGestureDetector detector) { 107 | // Do nothing. 108 | } 109 | }); 110 | 111 | public TermuxFloatView(Context context, AttributeSet attrs) { 112 | super(context, attrs); 113 | setAlpha(ALPHA_FOCUS); 114 | } 115 | 116 | private static int computeLayoutFlags(boolean withFocus) { 117 | if (withFocus) { 118 | return 0; 119 | } else { 120 | return WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 121 | } 122 | } 123 | 124 | public void initFloatView(TermuxFloatService service) { 125 | Logger.logDebug(LOG_TAG, "initFloatView"); 126 | 127 | // Load termux shared properties 128 | mProperties = new TermuxFloatAppSharedProperties(getContext()); 129 | 130 | // Load termux float shared preferences 131 | // This will also fail if TermuxConstants.TERMUX_FLOAT_PACKAGE_NAME does not equal applicationId 132 | mPreferences = TermuxFloatAppSharedPreferences.build(getContext(), true); 133 | if (mPreferences == null) { 134 | return; 135 | } 136 | 137 | mTermuxFloatSessionClient = new TermuxFloatSessionClient(service, this); 138 | 139 | mTerminalView = findViewById(R.id.terminal_view); 140 | mTermuxFloatViewClient = new TermuxFloatViewClient(this, mTermuxFloatSessionClient); 141 | mTerminalView.setTerminalViewClient(mTermuxFloatViewClient); 142 | mTermuxFloatViewClient.initFloatView(); 143 | 144 | mFloatingBubbleManager = new FloatingBubbleManager(this); 145 | initWindowControls(); 146 | } 147 | 148 | private void initWindowControls() { 149 | mWindowControls = findViewById(R.id.window_controls); 150 | mWindowControls.setOnClickListener(v -> changeFocus(true)); 151 | 152 | Button minimizeButton = findViewById(R.id.minimize_button); 153 | minimizeButton.setOnClickListener(v -> mFloatingBubbleManager.toggleBubble()); 154 | 155 | Button exitButton = findViewById(R.id.exit_button); 156 | exitButton.setOnClickListener(v -> exit()); 157 | } 158 | 159 | @Override 160 | protected void onAttachedToWindow() { 161 | super.onAttachedToWindow(); 162 | 163 | Point displaySize = new Point(); 164 | getDisplay().getSize(displaySize); 165 | DISPLAY_WIDTH = displaySize.x; 166 | DISPLAY_HEIGHT = displaySize.y; 167 | 168 | if (mTermuxFloatSessionClient != null) 169 | mTermuxFloatSessionClient.onAttachedToWindow(); 170 | } 171 | 172 | @Override 173 | protected void onDetachedFromWindow() { 174 | super.onDetachedFromWindow(); 175 | 176 | if (mTermuxFloatSessionClient != null) 177 | mTermuxFloatSessionClient.onDetachedFromWindow(); 178 | } 179 | 180 | @SuppressLint("RtlHardcoded") 181 | public void launchFloatingWindow() { 182 | int widthAndHeight = android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 183 | layoutParams.flags = computeLayoutFlags(true); 184 | layoutParams.width = widthAndHeight; 185 | layoutParams.height = widthAndHeight; 186 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 187 | layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; 188 | } else { 189 | layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE; 190 | } 191 | layoutParams.format = PixelFormat.RGBA_8888; 192 | 193 | layoutParams.gravity = Gravity.TOP | Gravity.LEFT; 194 | 195 | if (mPreferences != null) { 196 | layoutParams.x = mPreferences.getWindowX(); 197 | layoutParams.y = mPreferences.getWindowY(); 198 | layoutParams.width = mPreferences.getWindowWidth(); 199 | layoutParams.height = mPreferences.getWindowHeight(); 200 | } 201 | 202 | mWindowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); 203 | if (getWindowToken() == null) 204 | mWindowManager.addView(this, layoutParams); 205 | showTouchKeyboard(); 206 | } 207 | 208 | /** 209 | * Intercept touch events to obtain and loose focus on touch events. 210 | */ 211 | @Override 212 | public boolean onInterceptTouchEvent(MotionEvent event) { 213 | if (isInLongPressState) return true; 214 | 215 | getLocationOnScreen(location); 216 | int x = location[0]; 217 | int y = location[1]; 218 | float touchX = event.getRawX(); 219 | float touchY = event.getRawY(); 220 | 221 | if (didClickInsideWindowControls(touchX, touchY)) { 222 | // avoid unintended focus event if we are tapping on our window controls 223 | // so that keyboard doesn't possibly show briefly 224 | return false; 225 | } 226 | 227 | boolean clickedInside = (touchX >= x) && (touchX <= (x + layoutParams.width)) && (touchY >= y) && (touchY <= (y + layoutParams.height)); 228 | 229 | switch (event.getAction()) { 230 | case MotionEvent.ACTION_DOWN: 231 | if (!clickedInside) changeFocus(false); 232 | break; 233 | case MotionEvent.ACTION_UP: 234 | if (clickedInside) { 235 | changeFocus(true); 236 | showTouchKeyboard(); 237 | } 238 | break; 239 | } 240 | return false; 241 | } 242 | 243 | private boolean didClickInsideWindowControls(float touchX, float touchY) { 244 | if (mWindowControls.getVisibility() == View.GONE) { 245 | return false; 246 | } 247 | mWindowControls.getLocationOnScreen(windowControlsLocation); 248 | int controlsX = windowControlsLocation[0]; 249 | int controlsY = windowControlsLocation[1]; 250 | 251 | return (touchX >= controlsX && touchX <= controlsX + mWindowControls.getWidth()) && 252 | (touchY >= controlsY && touchY <= controlsY + mWindowControls.getHeight()); 253 | } 254 | 255 | void showTouchKeyboard() { 256 | mTerminalView.post(() -> KeyboardUtils.showSoftKeyboard(getContext(), mTerminalView)); 257 | 258 | } 259 | 260 | void hideTouchKeyboard() { 261 | mTerminalView.post(() -> KeyboardUtils.hideSoftKeyboard(getContext(), mTerminalView)); 262 | } 263 | 264 | void updateLongPressMode(boolean newValue) { 265 | isInLongPressState = newValue; 266 | mFloatingBubbleManager.updateLongPressBackgroundResource(isInLongPressState); 267 | setAlpha(newValue ? ALPHA_MOVING : (withFocus ? ALPHA_FOCUS : ALPHA_NOT_FOCUS)); 268 | if (newValue && !mFloatingBubbleManager.isMinimized()) 269 | Logger.showToast(getContext(), getContext().getString(R.string.after_long_press), false); 270 | } 271 | 272 | /** 273 | * Motion events should only be dispatched here when {@link #onInterceptTouchEvent(MotionEvent)} returns true. 274 | */ 275 | @SuppressLint("ClickableViewAccessibility") 276 | @Override 277 | public boolean onTouchEvent(MotionEvent event) { 278 | if (isInLongPressState) { 279 | mScaleDetector.onTouchEvent(event); 280 | if (mScaleDetector.isInProgress()) return true; 281 | switch (event.getAction()) { 282 | case MotionEvent.ACTION_MOVE: 283 | layoutParams.x = Math.min(DISPLAY_WIDTH - layoutParams.width, Math.max(0, initialX + (int) (event.getRawX() - initialTouchX))); 284 | layoutParams.y = Math.min(DISPLAY_HEIGHT - layoutParams.height, Math.max(0, initialY + (int) (event.getRawY() - initialTouchY))); 285 | mWindowManager.updateViewLayout(TermuxFloatView.this, layoutParams); 286 | if (mPreferences != null) { 287 | mPreferences.setWindowX(layoutParams.x); 288 | mPreferences.setWindowY(layoutParams.y); 289 | } 290 | break; 291 | case MotionEvent.ACTION_UP: 292 | updateLongPressMode(false); 293 | break; 294 | } 295 | return true; 296 | } 297 | return super.onTouchEvent(event); 298 | } 299 | 300 | /** 301 | * Visually indicate focus and show the soft input as needed. 302 | */ 303 | void changeFocus(boolean newFocus) { 304 | if (newFocus && mFloatingBubbleManager.isMinimized()) { 305 | mFloatingBubbleManager.displayAsFloatingWindow(); 306 | } 307 | if (newFocus == withFocus) { 308 | if (newFocus) showTouchKeyboard(); 309 | return; 310 | } 311 | withFocus = newFocus; 312 | layoutParams.flags = computeLayoutFlags(withFocus); 313 | if (getWindowToken() != null) 314 | mWindowManager.updateViewLayout(this, layoutParams); 315 | setAlpha(newFocus ? ALPHA_FOCUS : ALPHA_NOT_FOCUS); 316 | } 317 | 318 | public void closeFloatingWindow() { 319 | if (getWindowToken() != null) 320 | mWindowManager.removeView(this); 321 | 322 | mFloatingBubbleManager.cleanup(); 323 | mFloatingBubbleManager = null; 324 | } 325 | 326 | private void exit() { 327 | Intent exitIntent = new Intent(getContext(), TermuxFloatService.class).setAction(TermuxConstants.TERMUX_FLOAT_APP.TERMUX_FLOAT_SERVICE.ACTION_STOP_SERVICE); 328 | getContext().startService(exitIntent); 329 | } 330 | 331 | 332 | 333 | public boolean isVisible() { 334 | return isAttachedToWindow() && isShown(); 335 | } 336 | 337 | public TerminalView getTerminalView() { 338 | return mTerminalView; 339 | } 340 | 341 | public TermuxFloatViewClient getTermuxFloatViewClient() { 342 | return mTermuxFloatViewClient; 343 | } 344 | 345 | public TermuxFloatSessionClient getTermuxFloatSessionClient() { 346 | return mTermuxFloatSessionClient; 347 | } 348 | 349 | public TermuxFloatAppSharedPreferences getPreferences() { 350 | return mPreferences; 351 | } 352 | 353 | public TermuxFloatAppSharedProperties getProperties() { 354 | return mProperties; 355 | } 356 | 357 | 358 | public void reloadViewStyling() { 359 | // Leaving here for future support for termux-reload-settings 360 | if (mTermuxFloatSessionClient != null) 361 | mTermuxFloatSessionClient.onReload(); 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /app/src/main/java/com/termux/window/TermuxFloatViewClient.java: -------------------------------------------------------------------------------- 1 | package com.termux.window; 2 | 3 | import android.content.Context; 4 | import android.media.AudioManager; 5 | import android.view.InputDevice; 6 | import android.view.KeyEvent; 7 | import android.view.MotionEvent; 8 | 9 | import com.termux.shared.termux.terminal.TermuxTerminalViewClientBase; 10 | import com.termux.shared.view.KeyboardUtils; 11 | import com.termux.terminal.KeyHandler; 12 | import com.termux.terminal.TerminalEmulator; 13 | import com.termux.terminal.TerminalSession; 14 | 15 | public class TermuxFloatViewClient extends TermuxTerminalViewClientBase { 16 | 17 | private final TermuxFloatView mView; 18 | private final TermuxFloatSessionClient mTermuxFloatSessionClient; 19 | 20 | /** 21 | * Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. 22 | */ 23 | boolean mVirtualControlKeyDown, mVirtualFnKeyDown; 24 | 25 | public TermuxFloatViewClient(TermuxFloatView view, TermuxFloatSessionClient termuxFloatSessionClient) { 26 | mView = view; 27 | mTermuxFloatSessionClient = termuxFloatSessionClient; 28 | } 29 | 30 | /** 31 | * Should be called when TermuxFloatView.initFloatView() is called 32 | */ 33 | public void initFloatView() { 34 | mView.getTerminalView().setTextSize(mView.getPreferences().getFontSize()); 35 | 36 | // Set {@link TerminalView#TERMINAL_VIEW_KEY_LOGGING_ENABLED} value 37 | boolean isTerminalViewKeyLoggingEnabled = mView.getPreferences().isTerminalViewKeyLoggingEnabled(true); 38 | mView.getTerminalView().setIsTerminalViewKeyLoggingEnabled(isTerminalViewKeyLoggingEnabled); 39 | } 40 | 41 | /** 42 | * Should be called when {@link com.termux.view.TerminalView#mEmulator} is set 43 | */ 44 | @Override 45 | public void onEmulatorSet() { 46 | // This is being called every time float bubble is maximized 47 | mTermuxFloatSessionClient.checkForFontAndColors(); 48 | } 49 | 50 | 51 | 52 | @Override 53 | public float onScale(float scale) { 54 | if (scale < 0.9f || scale > 1.1f) { 55 | boolean increase = scale > 1.f; 56 | changeFontSize(increase); 57 | return 1.0f; 58 | } 59 | return scale; 60 | } 61 | 62 | @Override 63 | public boolean onLongPress(MotionEvent event) { 64 | mView.updateLongPressMode(true); 65 | mView.getLocationOnScreen(mView.location); 66 | mView.initialX = mView.location[0]; 67 | mView.initialY = mView.location[1]; 68 | mView.initialTouchX = event.getRawX(); 69 | mView.initialTouchY = event.getRawY(); 70 | return true; 71 | } 72 | 73 | @Override 74 | public boolean shouldBackButtonBeMappedToEscape() { 75 | return mView.getProperties().isBackKeyTheEscapeKey(); 76 | } 77 | 78 | @Override 79 | public boolean shouldEnforceCharBasedInput() { 80 | return mView.getProperties().isEnforcingCharBasedInput(); 81 | } 82 | 83 | @Override 84 | public boolean shouldUseCtrlSpaceWorkaround() { 85 | return mView.getProperties().isUsingCtrlSpaceWorkaround(); 86 | } 87 | 88 | @Override 89 | public void copyModeChanged(boolean copyMode) { 90 | 91 | } 92 | 93 | @Override 94 | public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession session) { 95 | if (handleVirtualKeys(keyCode, e, true)) return true; 96 | 97 | if (!mView.getProperties().areHardwareKeyboardShortcutsDisabled() && 98 | e.isCtrlPressed() && e.isAltPressed()) { 99 | // Get the unmodified code point: 100 | int unicodeChar = e.getUnicodeChar(0); 101 | 102 | if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || unicodeChar == 'n'/* next */) { 103 | // TODO: Toggle minimized or not. 104 | } else if (unicodeChar == 'f'/* full screen */) { 105 | // TODO: Toggle full screen. 106 | } else if (unicodeChar == 'k'/* keyboard */) { 107 | KeyboardUtils.toggleSoftKeyboard(mView.getContext()); 108 | } else if (unicodeChar == '+' || e.getUnicodeChar(KeyEvent.META_SHIFT_ON) == '+') { 109 | // We also check for the shifted char here since shift may be required to produce '+', 110 | // see https://github.com/termux/termux-api/issues/2 111 | changeFontSize(true); 112 | } else if (unicodeChar == '-') { 113 | changeFontSize(false); 114 | } 115 | return true; 116 | } 117 | 118 | return false; 119 | } 120 | 121 | @Override 122 | public boolean onKeyUp(int keyCode, KeyEvent e) { 123 | return handleVirtualKeys(keyCode, e, false); 124 | } 125 | 126 | 127 | @Override 128 | public boolean readControlKey() { 129 | return mVirtualControlKeyDown; 130 | } 131 | 132 | @Override 133 | public boolean readAltKey() { 134 | return false; 135 | } 136 | 137 | @Override 138 | public boolean onCodePoint(int codePoint, boolean ctrlDown, TerminalSession session) { 139 | if (mVirtualFnKeyDown) { 140 | int resultingKeyCode = -1; 141 | int resultingCodePoint = -1; 142 | boolean altDown = false; 143 | int lowerCase = Character.toLowerCase(codePoint); 144 | switch (lowerCase) { 145 | // Arrow keys. 146 | case 'w': 147 | resultingKeyCode = KeyEvent.KEYCODE_DPAD_UP; 148 | break; 149 | case 'a': 150 | resultingKeyCode = KeyEvent.KEYCODE_DPAD_LEFT; 151 | break; 152 | case 's': 153 | resultingKeyCode = KeyEvent.KEYCODE_DPAD_DOWN; 154 | break; 155 | case 'd': 156 | resultingKeyCode = KeyEvent.KEYCODE_DPAD_RIGHT; 157 | break; 158 | 159 | // Page up and down. 160 | case 'p': 161 | resultingKeyCode = KeyEvent.KEYCODE_PAGE_UP; 162 | break; 163 | case 'n': 164 | resultingKeyCode = KeyEvent.KEYCODE_PAGE_DOWN; 165 | break; 166 | 167 | // Some special keys: 168 | case 't': 169 | resultingKeyCode = KeyEvent.KEYCODE_TAB; 170 | break; 171 | case 'i': 172 | resultingKeyCode = KeyEvent.KEYCODE_INSERT; 173 | break; 174 | case 'h': 175 | resultingCodePoint = '~'; 176 | break; 177 | 178 | // Special characters to input. 179 | case 'u': 180 | resultingCodePoint = '_'; 181 | break; 182 | case 'l': 183 | resultingCodePoint = '|'; 184 | break; 185 | 186 | // Function keys. 187 | case '1': 188 | case '2': 189 | case '3': 190 | case '4': 191 | case '5': 192 | case '6': 193 | case '7': 194 | case '8': 195 | case '9': 196 | resultingKeyCode = (codePoint - '1') + KeyEvent.KEYCODE_F1; 197 | break; 198 | case '0': 199 | resultingKeyCode = KeyEvent.KEYCODE_F10; 200 | break; 201 | 202 | // Other special keys. 203 | case 'e': 204 | resultingCodePoint = /*Escape*/ 27; 205 | break; 206 | case '.': 207 | resultingCodePoint = /*^.*/ 28; 208 | break; 209 | 210 | case 'b': // alt+b, jumping backward in readline. 211 | case 'f': // alf+f, jumping forward in readline. 212 | case 'x': // alt+x, common in emacs. 213 | resultingCodePoint = lowerCase; 214 | altDown = true; 215 | break; 216 | 217 | // Volume control. 218 | case 'v': 219 | resultingCodePoint = -1; 220 | AudioManager audio = (AudioManager) mView.getContext().getSystemService(Context.AUDIO_SERVICE); 221 | audio.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME, AudioManager.USE_DEFAULT_STREAM_TYPE, AudioManager.FLAG_SHOW_UI); 222 | break; 223 | } 224 | 225 | if (resultingKeyCode != -1) { 226 | TerminalEmulator term = session.getEmulator(); 227 | session.write(KeyHandler.getCode(resultingKeyCode, 0, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode())); 228 | } else if (resultingCodePoint != -1) { 229 | session.writeCodePoint(altDown, resultingCodePoint); 230 | } 231 | return true; 232 | } 233 | 234 | return false; 235 | } 236 | 237 | /** 238 | * Handle dedicated volume buttons as virtual keys if applicable. 239 | */ 240 | private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) { 241 | InputDevice inputDevice = event.getDevice(); 242 | if (mView.getProperties().areVirtualVolumeKeysDisabled()) { 243 | return false; 244 | } else if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) { 245 | // Do not steal dedicated buttons from a full external keyboard. 246 | return false; 247 | } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { 248 | mVirtualControlKeyDown = down; 249 | return true; 250 | } else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) { 251 | mVirtualFnKeyDown = down; 252 | return true; 253 | } 254 | return false; 255 | } 256 | 257 | 258 | 259 | public void changeFontSize(boolean increase) { 260 | mView.getPreferences().changeFontSize(increase); 261 | mView.getTerminalView().setTextSize(mView.getPreferences().getFontSize()); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /app/src/main/java/com/termux/window/settings/properties/TermuxFloatAppSharedProperties.java: -------------------------------------------------------------------------------- 1 | package com.termux.window.settings.properties; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.annotation.NonNull; 6 | 7 | import com.termux.shared.termux.TermuxConstants; 8 | import com.termux.shared.termux.settings.properties.TermuxPropertyConstants; 9 | import com.termux.shared.termux.settings.properties.TermuxSharedProperties; 10 | 11 | public class TermuxFloatAppSharedProperties extends TermuxSharedProperties { 12 | 13 | private static final String LOG_TAG = "TermuxFloatAppSharedProperties"; 14 | 15 | public TermuxFloatAppSharedProperties(@NonNull Context context) { 16 | super(context, TermuxConstants.TERMUX_FLOAT_APP_NAME, 17 | TermuxConstants.TERMUX_FLOAT_PROPERTIES_FILE_PATHS_LIST, 18 | TermuxPropertyConstants.TERMUX_APP_PROPERTIES_LIST, 19 | new SharedPropertiesParserClient()); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/floating_window_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/floating_window_background_resize.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_exit_icon.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 18 | 19 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 14 | 24 | 25 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_minimize_icon.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_button_with_outline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 |