diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..d34bce6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,35 @@ +--- +name: Bug report +about: Create a report to help us improve Termux:Float application + +--- + + + +**Problem description** + + +**Steps to reproduce** + + +**Expected behavior** + + +**Additional information** + +* Termux application version: +* Android OS version: +* Device model: diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..098c8c3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest a new feature for Termux:Float application + +--- + + + +**Feature description** + 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/github_action_build.yml b/.github/workflows/github_action_build.yml new file mode 100644 index 0000000..9134698 --- /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-float-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_FLOAT_APP__BUILD__APP_VERSION_NAME="${RELEASE_VERSION_NAME/v/}" # Used by app/build.gradle + export TERMUX_FLOAT_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@v4 + 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..1ac69ba --- /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-float-app_$APK_VERSION_TAG" + + echo "Building APK file for '$RELEASE_VERSION_NAME' release with '$APK_VERSION_TAG' tag" + export TERMUX_FLOAT_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 3daa7a4..9da7ecf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,45 +1,8 @@ -# From https://gist.github.com/iainconnor/8605514 -# with the addition of the /captures below. -/captures - -# Built application files build/ -*.apk -*.so -.externalNativeBuild - -# Crashlytics configuations -com_crashlytics_export_strings.xml - -# Local configuration file (sdk path, etc) -local.properties - -# Gradle generated files +release/ .gradle/ - -# Signing files -.signing/ - -# User-specific configurations -.idea/libraries/ -.idea/workspace.xml -.idea/tasks.xml -.idea/.name -.idea/compiler.xml -.idea/copyright/profiles_settings.xml -.idea/encodings.xml -.idea/misc.xml -.idea/modules.xml -.idea/scopes/scope_settings.xml -.idea/vcs.xml -.idea/dictionaries/ +.idea/ *.iml -# OS-specific files -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db +local.properties +github.properties diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 7ac24c7..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index 7f68460..0000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md index 7e951d4..a1639fe 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,3 +1,5 @@ -Released under [the GPLv3 license](https://www.gnu.org/licenses/gpl.html). +The `termux/termux-float` repository is released under [GPLv3 only](https://www.gnu.org/licenses/gpl-3.0.html) license. -Contains code from `Terminal Emulator for Android` by which is released under [the Apache License 2.0](https://www.apache.org/licenses/). +### Exceptions + +- [Terminal Emulator for Android](https://github.com/jackpal/Android-Terminal-Emulator) code is used which is released under [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). Check [`terminal-view`](https://github.com/termux/termux-app/tree/master/terminal-view) and [`terminal-emulator`](https://github.com/termux/termux-app/tree/master/terminal-emulator) libraries. diff --git a/README.md b/README.md index 1bf60e9..469ae98 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,99 @@ -Termux:Float ------------- +# Termux:Float + +[![Build status](https://github.com/termux/termux-float/workflows/Build/badge.svg)](https://github.com/termux/termux-float/actions) [![Join the chat at https://gitter.im/termux/termux](https://badges.gitter.im/termux/termux.svg)](https://gitter.im/termux/termux) -A [Termux](https://termux.com/) add-on app to show the terminal in a floating terminal window. +A [Termux] plugin app to show the terminal in a floating terminal window. +## + + + +### Contents +- [Installation](#Installation) +- [Terminal and App Settings](#Terminal-and-App-Settings) +- [Debugging](#Debugging) +- [Worthy Of Note](#Worthy-Of-Note) +- [For Maintainers and Contributors](#For-Maintainers-and-Contributors) +- [Forking](#Forking) +## + + + +### Installation + +Latest version is `v0.17.0`. + +**Check [`termux-app` Installation](https://github.com/termux/termux-app#Installation) for details before reading forward.** + +### F-Droid + +`Termux:Float` application can be obtained from `F-Droid` from [here](https://f-droid.org/en/packages/com.termux.window). + +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. + +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. + +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: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). + +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.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. +## + + + +### Terminal and App Settings + +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. + +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. + +``` +mkdir -p ~/.termux +nano ~/.termux/termux.float.properties +``` + +## + + + +### Debugging + +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. + +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). + +##### Log Levels +- `Off` - Log nothing +- `Normal` - Start logging error, warn and info messages and stacktraces +- `Debug` - Start logging debug messages +- `Verbose` - Start logging verbose messages +## + + + +## For Maintainers and Contributors + +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:Float on Google Play](https://play.google.com/store/apps/details?id=com.termux.window) -- [Termux:Float on F-Droid](https://f-droid.org/repository/browse/?fdid=com.termux.window) -When developing (or packaging), note that this app needs to be signed with the same key as the main Termux app in order to have the permission to modify the required font or color files. +[Termux]: https://termux.dev 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 2d86fec..432ba1f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,29 +1,98 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 27 - buildToolsVersion "27.0.1" + namespace "com.termux.window" + + compileSdk project.properties.compileSdkVersion.toInteger() + def appVersionName = System.getenv("TERMUX_FLOAT_APP__BUILD__APP_VERSION_NAME") ?: "" + def apkVersionTag = System.getenv("TERMUX_FLOAT_APP__BUILD__APK_VERSION_TAG") ?: "" defaultConfig { applicationId "com.termux.window" - minSdkVersion 21 - targetSdkVersion 27 - versionCode 11 - versionName "0.11" + minSdk project.properties.minSdkVersion.toInteger() + targetSdk project.properties.targetSdkVersion.toInteger() + versionCode 1001 + versionName "0.17.0" + + if (appVersionName) versionName = appVersionName + validateVersionName(versionName) + + manifestPlaceholders.TERMUX_PACKAGE_NAME = "com.termux" + manifestPlaceholders.TERMUX_APP_NAME = "Termux" + manifestPlaceholders.TERMUX_FLOAT_APP_NAME = "Termux:Float" + } + + signingConfigs { + debug { + storeFile file('testkey_untrusted.jks') + keyAlias 'alias' + storePassword 'xrj45yWGLbsO7W0v' + keyPassword 'xrj45yWGLbsO7W0v' + } } buildTypes { release { minifyEnabled true - shrinkResources true + shrinkResources false // Reproducible builds proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } + + debug { + signingConfig signingConfigs.debug + zipAlignEnabled true + } + } + + compileOptions { + // 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-float-app_" + + (apkVersionTag ? apkVersionTag : "v" + versionName + "+" + variant.buildType.name) + ".apk") + } + } + + packagingOptions { + // Remove terminal-shared JNI libs added via termux-shared dependency + exclude "lib/*/liblocal-socket.so" } } dependencies { - compile 'com.android.support:support-annotations:27.0.0' - compile 'com.termux:terminal-view:0.50' + coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.1.5" + + testImplementation "junit:junit:4.13.2" + + implementation "androidx.annotation:annotation:1.9.1" + + implementation "com.termux.termux-app:termux-shared:8aca6dbbf4" + implementation "com.termux.termux-app:terminal-view: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.termux:terminal-view:0.118.0" + + implementation "com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava" +} + +task versionName { + doLast { + print android.defaultConfig.versionName + } +} - testCompile 'junit:junit:4.12' +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 2e56e60..3ebfa45 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -7,19 +7,4 @@ # 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 *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile +-dontobfuscate diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1c513a2..b3aed65 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,21 +1,22 @@ + xmlns:tools="http://schemas.android.com/tools" + android:sharedUserId="${TERMUX_PACKAGE_NAME}"> + - + android:theme="@style/AppTheme" + tools:targetApi="m"> - * Note that the javadoc for that method says it cannot be zero. - */ - private static final int NOTIFICATION_ID = 0xdead1337; - - private static final int MIN_FONTSIZE = 16; - private static final int DEFAULT_FONTSIZE = 24; - private static final String FONTSIZE_KEY = "fontsize"; private TermuxFloatView mFloatingWindow; - private int mFontSize; + + private TermuxSession mSession; + private boolean mVisibleWindow = true; + private static final String LOG_TAG = "TermuxFloatService"; + @Override public IBinder onBind(Intent intent) { return null; } - @SuppressLint({"InflateParams"}) @Override public void onCreate() { - super.onCreate(); + runStartForeground(); + TermuxFloatApplication.setLogConfig(this, false); + Logger.logVerbose(LOG_TAG, "onCreate"); + } - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - try { - mFontSize = Integer.parseInt(prefs.getString(FONTSIZE_KEY, Integer.toString(DEFAULT_FONTSIZE))); - } catch (NumberFormatException | ClassCastException e) { - mFontSize = DEFAULT_FONTSIZE; - } + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Logger.logDebug(LOG_TAG, "onStartCommand"); - TermuxFloatView floatingWindow = (TermuxFloatView) ((LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE)).inflate(R.layout.activity_main, null); - floatingWindow.initializeFloatingWindow(); - floatingWindow.mTerminalView.setTextSize(mFontSize); + // Run again in case service is already started and onCreate() is not called + runStartForeground(); - TerminalSession session = createTermSession(); - floatingWindow.mTerminalView.attachSession(session); + if (mFloatingWindow == null && !initializeFloatView()) + return Service.START_NOT_STICKY; - try { - floatingWindow.launchFloatingWindow(); - } catch (Exception e) { - // Settings.canDrawOverlays() does not work (always returns false, perhaps due to sharedUserId?). - // So instead we catch the exception and prompt here. - startActivity(new Intent(this, TermuxFloatPermissionActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); - stopSelf(); - return; + String action = null; + if (intent != null) { + Logger.logVerboseExtended(LOG_TAG, "Received intent:\n" + IntentUtils.getIntentString(intent)); + action = intent.getAction(); + } + + if (action != null) { + switch (action) { + case TERMUX_FLOAT_SERVICE.ACTION_STOP_SERVICE: + actionStopService(); + break; + case TERMUX_FLOAT_SERVICE.ACTION_SHOW: + setVisible(true); + break; + case TERMUX_FLOAT_SERVICE.ACTION_HIDE: + setVisible(false); + break; + default: + Logger.logError(LOG_TAG, "Invalid action: \"" + action + "\""); + break; + } + } else if (!mVisibleWindow) { + // Show window if hidden when launched through launcher icon. + setVisible(true); } - mFloatingWindow = floatingWindow; + return Service.START_NOT_STICKY; + + } - Toast toast = Toast.makeText(this, R.string.initial_instruction_toast, Toast.LENGTH_LONG); - toast.setGravity(Gravity.CENTER, 0, 0); - TextView v = toast.getView().findViewById(android.R.id.message); - if (v != null) v.setGravity(Gravity.CENTER); - toast.show(); + @Override + public void onDestroy() { + super.onDestroy(); + Logger.logVerbose(LOG_TAG, "onDestroy"); - startForeground(NOTIFICATION_ID, buildNotification()); + if (mFloatingWindow != null) + mFloatingWindow.closeFloatingWindow(); + + runStopForeground(); + } + /** Request to stop service. */ + public void requestStopService() { + Logger.logDebug(LOG_TAG, "Requesting to stop service"); + runStopForeground(); + stopSelf(); } - @RequiresApi(api = Build.VERSION_CODES.O) + /** Process action to stop service. */ + private void actionStopService() { + if (mSession != null) + mSession.killIfExecuting(this, false); + requestStopService(); + } + + /** Make service run in foreground mode. */ + private void runStartForeground() { + setupNotificationChannel(); + startForeground(TermuxConstants.TERMUX_FLOAT_APP_NOTIFICATION_ID, buildNotification()); + } + + /** Make service leave foreground mode. */ + private void runStopForeground() { + stopForeground(true); + } + + + private void setupNotificationChannel() { - String channelName = "Termux"; - String channelDescription = "Notifications from Termux"; - int importance = NotificationManager.IMPORTANCE_LOW; - - NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, importance); - channel.setDescription(channelDescription); - NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - manager.createNotificationChannel(channel); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return; + + NotificationUtils.setupNotificationChannel(this, TermuxConstants.TERMUX_FLOAT_APP_NOTIFICATION_CHANNEL_ID, + TermuxConstants.TERMUX_FLOAT_APP_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW); } private Notification buildNotification() { final Resources res = getResources(); - final String contentTitle = res.getString(R.string.notification_title); - final String contentText = res.getString(mVisibleWindow ? R.string.notification_message_visible : R.string.notification_message_hidden); - - final String intentAction = mVisibleWindow ? ACTION_HIDE : ACTION_SHOW; - Intent actionIntent = new Intent(this, TermuxFloatService.class).setAction(intentAction); - - Notification.Builder builder = new Notification.Builder(this).setContentTitle(contentTitle).setContentText(contentText) - .setPriority(Notification.PRIORITY_MIN).setSmallIcon(R.mipmap.ic_service_notification) - .setColor(0xFF000000) - .setContentIntent(PendingIntent.getService(this, 0, actionIntent, 0)) - .setOngoing(true) - .setShowWhen(false); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - setupNotificationChannel(); - builder.setChannelId(NOTIFICATION_CHANNEL_ID); - } - //final int messageId = mVisibleWindow ? R.string.toggle_hide : R.string.toggle_show; - //builder.addAction(android.R.drawable.ic_menu_preferences, res.getString(messageId), PendingIntent.getService(this, 0, actionIntent, 0)); + final String notificationText = res.getString(mVisibleWindow ? R.string.notification_message_visible : R.string.notification_message_hidden); + + final String intentAction = mVisibleWindow ? TERMUX_FLOAT_SERVICE.ACTION_HIDE : TERMUX_FLOAT_SERVICE.ACTION_SHOW; + Intent notificationIntent = new Intent(this, TermuxFloatService.class).setAction(intentAction); + PendingIntent contentIntent = PendingIntent.getService(this, 0, notificationIntent, + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE : 0); + + // Build the notification + Notification.Builder builder = NotificationUtils.geNotificationBuilder(this, + TermuxConstants.TERMUX_FLOAT_APP_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_LOW, + TermuxConstants.TERMUX_FLOAT_APP_NAME, notificationText, null, + contentIntent, null, NotificationUtils.NOTIFICATION_MODE_SILENT); + if (builder == null) return null; + + // No need to show a timestamp: + builder.setShowWhen(false); + + // Set notification icon + builder.setSmallIcon(R.mipmap.ic_service_notification); + + // Set background color for small notification icon + builder.setColor(0xFF000000); + + // TermuxSessions are always ongoing + builder.setOngoing(true); + + // Set Exit button action + Intent exitIntent = new Intent(this, TermuxFloatService.class).setAction(TERMUX_FLOAT_SERVICE.ACTION_STOP_SERVICE); + builder.addAction(android.R.drawable.ic_delete, res.getString(R.string.notification_action_exit), + PendingIntent.getService(this, 0, exitIntent, + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE : 0)); + return builder.build(); } - @SuppressLint("Wakelock") - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - String action = intent.getAction(); - if (ACTION_HIDE.equals(action)) { - setVisible(false); - } else if (ACTION_SHOW.equals(action)) { - setVisible(true); - } else if (!mVisibleWindow) { - // Show window if hidden when launched through launcher icon. - setVisible(true); + + @SuppressLint("InflateParams") + private boolean initializeFloatView() { + boolean floatWindowWasNull = false; + if (mFloatingWindow == null) { + mFloatingWindow = (TermuxFloatView) ((LayoutInflater) + getSystemService(Context.LAYOUT_INFLATER_SERVICE)).inflate(R.layout.activity_main, null); + floatWindowWasNull = true; } - return Service.START_NOT_STICKY; - } - @Override - public void onDestroy() { - super.onDestroy(); - if (mFloatingWindow != null) mFloatingWindow.closeFloatingWindow(); + mFloatingWindow.initFloatView(this); + + mSession = createTermuxSession( + new ExecutionCommand(0, null, null, null, mFloatingWindow.getProperties().getDefaultWorkingDirectory(), ExecutionCommand.Runner.TERMINAL_SESSION.getName(), false), null); + if (mSession == null) + return false; + mFloatingWindow.getTerminalView().attachSession(mSession.getTerminalSession()); + + try { + mFloatingWindow.launchFloatingWindow(); + } catch (Exception e) { + Logger.logStackTrace(LOG_TAG, e); + // Settings.canDrawOverlays() does not work (always returns false, perhaps due to sharedUserId?). + // So instead we catch the exception and prompt here. + startActivity(new Intent(this, TermuxFloatPermissionActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + requestStopService(); + return false; + } + + if (floatWindowWasNull) + Logger.showToast(this, getString(R.string.initial_instruction_toast), true); + + return true; } private void setVisible(boolean newVisibility) { mVisibleWindow = newVisibility; mFloatingWindow.setVisibility(newVisibility ? View.VISIBLE : View.GONE); - ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, buildNotification()); + ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).notify(TermuxConstants.TERMUX_FLOAT_APP_NOTIFICATION_ID, buildNotification()); } - public void changeFontSize(boolean increase) { - mFontSize += (increase ? 1 : -1) * 2; - mFontSize = Math.max(MIN_FONTSIZE, mFontSize); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - prefs.edit().putString(FONTSIZE_KEY, Integer.toString(mFontSize)).apply(); - mFloatingWindow.mTerminalView.setTextSize(mFontSize); - } + /** Create a {@link TermuxSession}. */ + @Nullable + public synchronized TermuxSession createTermuxSession(ExecutionCommand executionCommand, String sessionName) { + if (executionCommand == null) return null; - // XXX: Keep in sync with TermuxService.java. - @SuppressLint("SdCardPath") - TerminalSession createTermSession() { - new File(HOME_PATH).mkdirs(); - - final String termEnv = "TERM=xterm-256color"; - final String homeEnv = "HOME=" + TermuxFloatService.HOME_PATH; - final String prefixEnv = "PREFIX=" + TermuxFloatService.PREFIX_PATH; - final String androidRootEnv = "ANDROID_ROOT=" + System.getenv("ANDROID_ROOT"); - final String androidDataEnv = "ANDROID_DATA=" + System.getenv("ANDROID_DATA"); - // EXTERNAL_STORAGE is needed for /system/bin/am to work on at least - // Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3. - final String externalStorageEnv = "EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE"); - final String ps1Env = "PS1=$ "; - final String ldEnv = "LD_LIBRARY_PATH=" + TermuxFloatService.PREFIX_PATH + "/lib"; - final String langEnv = "LANG=en_US.UTF-8"; - final String pathEnv = "PATH=" + TermuxFloatService.PREFIX_PATH + "/bin:" + TermuxFloatService.PREFIX_PATH + "/bin/applets"; - String[] env = new String[]{termEnv, homeEnv, prefixEnv, ps1Env, ldEnv, langEnv, pathEnv, androidRootEnv, androidDataEnv, externalStorageEnv}; - - String executablePath = null; - String[] args; - String shellName = null; - - for (String shellBinary : new String[]{"login", "bash", "zsh"}) { - File shellFile = new File(PREFIX_PATH + "/bin/" + shellBinary); - if (shellFile.canExecute()) { - executablePath = shellFile.getAbsolutePath(); - shellName = "-" + shellBinary; - break; - } - } + Logger.logDebug(LOG_TAG, "Creating \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession"); - if (executablePath == null) { - // Fall back to system shell as last resort: - executablePath = "/system/bin/sh"; - shellName = "-sh"; + if (ExecutionCommand.Runner.APP_SHELL.getName().equals(executionCommand.runner)) { + Logger.logDebug(LOG_TAG, "Ignoring a background execution command passed to createTermuxSession()"); + return null; } - args = new String[]{shellName}; - - return new TerminalSession(executablePath, HOME_PATH, args, env, new TerminalSession.SessionChangedCallback() { - @Override - public void onTitleChanged(TerminalSession changedSession) { - // Ignore for now. - } - - @Override - public void onTextChanged(TerminalSession changedSession) { - mFloatingWindow.mTerminalView.onScreenUpdated(); - } - - @Override - public void onSessionFinished(TerminalSession finishedSession) { - stopSelf(); - } + if (Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE) + Logger.logVerboseExtended(LOG_TAG, executionCommand.toString()); + + executionCommand.shellName = sessionName; + executionCommand.terminalTranscriptRows = mFloatingWindow.getProperties().getTerminalTranscriptRows(); + TermuxSession newTermuxSession = TermuxSession.execute(this, executionCommand, + mFloatingWindow.getTermuxFloatSessionClient(), null, new TermuxShellEnvironment(), + null, executionCommand.isPluginExecutionCommand); + if (newTermuxSession == null) { + Logger.logError(LOG_TAG, "Failed to execute new TermuxSession command for:\n" + executionCommand.getCommandIdAndLabelLogString()); + return null; + } - @Override - public void onClipboardText(TerminalSession pastingSession, String text) { - ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(text))); - } + // Emulator won't be set at this point so colors won't be set by TermuxFloatSessionClient.checkForFontAndColors() + mFloatingWindow.reloadViewStyling(); - @Override - public void onBell(TerminalSession riningSession) { - ((Vibrator) getSystemService(Context.VIBRATOR_SERVICE)).vibrate(50); - } + return newTermuxSession; + } - @Override - public void onColorsChanged(TerminalSession session) { + public TermuxSession getTermuxSession() { + return mSession; + } - } - }); + public TerminalSession getCurrentSession() { + return mSession != null ? mSession.getTerminalSession() : null; } } diff --git a/app/src/main/java/com/termux/window/TermuxFloatSessionClient.java b/app/src/main/java/com/termux/window/TermuxFloatSessionClient.java new file mode 100644 index 0000000..135fc4f --- /dev/null +++ b/app/src/main/java/com/termux/window/TermuxFloatSessionClient.java @@ -0,0 +1,191 @@ +package com.termux.window; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.graphics.Typeface; +import android.media.AudioAttributes; +import android.media.SoundPool; +import android.text.TextUtils; + +import com.termux.shared.logger.Logger; +import com.termux.shared.termux.TermuxConstants; +import com.termux.shared.termux.settings.properties.TermuxPropertyConstants; +import com.termux.shared.termux.terminal.TermuxTerminalSessionClientBase; +import com.termux.shared.termux.terminal.io.BellHandler; +import com.termux.terminal.TerminalColors; +import com.termux.terminal.TerminalSession; +import com.termux.terminal.TextStyle; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.Properties; + +public class TermuxFloatSessionClient extends TermuxTerminalSessionClientBase { + + private final TermuxFloatService mService; + private final TermuxFloatView mView; + + private SoundPool mBellSoundPool; + + private int mBellSoundId; + + private static final String LOG_TAG = "TermuxFloatSessionClient"; + + public TermuxFloatSessionClient(TermuxFloatService service, TermuxFloatView view) { + mService = service; + mView = view; + } + + /** + * Should be called when TermuxFloatView.onAttachedToWindow() is called + */ + public void onAttachedToWindow() { + // Just initialize the mBellSoundPool and load the sound, otherwise bell might not run + // the first time bell key is pressed and play() is called, since sound may not be loaded + // quickly enough before the call to play(). https://stackoverflow.com/questions/35435625 + loadBellSoundPool(); + } + + /** + * Should be called when TermuxFloatView.onDetachedFromWindow() is called + */ + public void onDetachedFromWindow() { + // Release mBellSoundPool resources, specially to prevent exceptions like the following to be thrown + // java.util.concurrent.TimeoutException: android.media.SoundPool.finalize() timed out after 10 seconds + // Bell is not played in background anyways + // Related: https://stackoverflow.com/a/28708351/14686958 + releaseBellSoundPool(); + } + + /** + * Should be called when TermuxFloatView.onReload() is called + */ + public void onReload() { + checkForFontAndColors(); + } + + + + @Override + public void onTextChanged(TerminalSession changedSession) { + if (!mView.isVisible()) return; + + mView.getTerminalView().onScreenUpdated(); + } + + @Override + public void onSessionFinished(TerminalSession finishedSession) { + mService.requestStopService(); + } + + @Override + public void onCopyTextToClipboard(TerminalSession pastingSession, String text) { + ClipboardManager clipboard = (ClipboardManager) mService.getSystemService(Context.CLIPBOARD_SERVICE); + clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(text))); + } + + @Override + public void onPasteTextFromClipboard(TerminalSession session) { + if (!mView.isVisible()) return; + + ClipboardManager clipboard = (ClipboardManager) mService.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clipData = clipboard.getPrimaryClip(); + if (clipData != null) { + CharSequence paste = clipData.getItemAt(0).coerceToText(mService); + if (!TextUtils.isEmpty(paste)) mView.getTerminalView().mEmulator.paste(paste.toString()); + } + } + + @Override + public void onBell(TerminalSession session) { + if (!mView.isVisible()) return; + + int bellBehaviour = mView.getProperties().getBellBehaviour(); + if (bellBehaviour == TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_VIBRATE) { + BellHandler.getInstance(mService).doBell(); + } else if (bellBehaviour == TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_BEEP) { + loadBellSoundPool(); + if (mBellSoundPool != null) + mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f); + } else if (bellBehaviour == TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_IGNORE) { + // Ignore the bell character. + } + } + + @Override + public void onColorsChanged(TerminalSession changedSession) { + updateBackgroundColor(); + } + + + @Override + public Integer getTerminalCursorStyle() { + return mView.getProperties().getTerminalCursorStyle(); + } + + + /** Load mBellSoundPool */ + private synchronized void loadBellSoundPool() { + if (mBellSoundPool == null) { + mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes( + new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build(); + + try { + mBellSoundId = mBellSoundPool.load(mService, com.termux.shared.R.raw.bell, 1); + } catch (Exception e){ + // Catch java.lang.RuntimeException: Unable to resume activity {com.termux/com.termux.app.TermuxActivity}: android.content.res.Resources$NotFoundException: File res/raw/bell.ogg from drawable resource ID + Logger.logStackTraceWithMessage(LOG_TAG, "Failed to load bell sound pool", e); + } + } + } + + /** Release mBellSoundPool resources */ + private synchronized void releaseBellSoundPool() { + if (mBellSoundPool != null) { + mBellSoundPool.release(); + mBellSoundPool = null; + } + } + + + + public void checkForFontAndColors() { + try { + File colorsFile = TermuxConstants.TERMUX_COLOR_PROPERTIES_FILE; + File fontFile = TermuxConstants.TERMUX_FONT_FILE; + + final Properties props = new Properties(); + if (colorsFile.isFile()) { + try (InputStream in = new FileInputStream(colorsFile)) { + props.load(in); + } + } + + TerminalColors.COLOR_SCHEME.updateWith(props); + TerminalSession session = mService.getCurrentSession(); + if (session != null && session.getEmulator() != null) { + session.getEmulator().mColors.reset(); + } + + updateBackgroundColor(); + + final Typeface newTypeface = (fontFile.exists() && fontFile.length() > 0) ? Typeface.createFromFile(fontFile) : Typeface.MONOSPACE; + mView.getTerminalView().setTypeface(newTypeface); + } catch (Exception e) { + Logger.logStackTraceWithMessage(LOG_TAG, "Error in checkForFontAndColors()", e); + } + } + + public void updateBackgroundColor() { + //if (!mView.isVisible()) return; + + TerminalSession session = mService.getCurrentSession(); + if (session != null && session.getEmulator() != null) { + mView.getTerminalView().setBackgroundColor(session.getEmulator().mColors.mCurrentColors[TextStyle.COLOR_INDEX_BACKGROUND]); + } + } + +} diff --git a/app/src/main/java/com/termux/window/TermuxFloatView.java b/app/src/main/java/com/termux/window/TermuxFloatView.java index 909731b..dff9e46 100644 --- a/app/src/main/java/com/termux/window/TermuxFloatView.java +++ b/app/src/main/java/com/termux/window/TermuxFloatView.java @@ -2,25 +2,30 @@ import android.annotation.SuppressLint; import android.content.Context; +import android.content.Intent; import android.graphics.PixelFormat; import android.graphics.Point; -import android.graphics.Typeface; import android.os.Build; import android.util.AttributeSet; -import android.util.Log; import android.view.Gravity; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.ScaleGestureDetector.OnScaleGestureListener; +import android.view.View; +import android.view.ViewGroup; import android.view.WindowManager; -import android.view.inputmethod.InputMethodManager; +import android.widget.Button; import android.widget.LinearLayout; -import android.widget.Toast; -import com.termux.terminal.EmulatorDebug; +import com.termux.shared.logger.Logger; +import com.termux.shared.termux.TermuxConstants; +import com.termux.shared.termux.settings.preferences.TermuxFloatAppSharedPreferences; +import com.termux.shared.view.KeyboardUtils; +import com.termux.terminal.TerminalSession; +import com.termux.terminal.TerminalSessionClient; import com.termux.view.TerminalView; - -import java.io.File; +import com.termux.view.TerminalViewClient; +import com.termux.window.settings.properties.TermuxFloatAppSharedProperties; public class TermuxFloatView extends LinearLayout { @@ -32,9 +37,32 @@ public class TermuxFloatView extends LinearLayout { final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(); WindowManager mWindowManager; - InputMethodManager imm; - TerminalView mTerminalView; + private TerminalView mTerminalView; + ViewGroup mWindowControls; + FloatingBubbleManager mFloatingBubbleManager; + + /** + * The {@link TerminalViewClient} interface implementation to allow for communication between + * {@link TerminalView} and {@link TermuxFloatView}. + */ + TermuxFloatViewClient mTermuxFloatViewClient; + + /** + * The {@link TerminalSessionClient} interface implementation to allow for communication between + * {@link TerminalSession} and {@link TermuxFloatService}. + */ + TermuxFloatSessionClient mTermuxFloatSessionClient; + + /** + * Termux Float app shared preferences manager. + */ + private TermuxFloatAppSharedPreferences mPreferences; + + /** + * Termux app shared properties manager, loaded from termux.properties + */ + private TermuxFloatAppSharedProperties mProperties; private boolean withFocus = true; int initialX; @@ -46,6 +74,10 @@ public class TermuxFloatView extends LinearLayout { final int[] location = new int[2]; + final int[] windowControlsLocation = new int[2]; + + private static final String LOG_TAG = "TermuxFloatView"; + final ScaleGestureDetector mScaleDetector = new ScaleGestureDetector(getContext(), new OnScaleGestureListener() { private static final int MIN_SIZE = 50; @@ -63,7 +95,10 @@ public boolean onScale(ScaleGestureDetector detector) { layoutParams.width = Math.max(MIN_SIZE, layoutParams.width); layoutParams.height = Math.max(MIN_SIZE, layoutParams.height); mWindowManager.updateViewLayout(TermuxFloatView.this, layoutParams); - TermuxFloatPrefs.saveWindowSize(getContext(), layoutParams.width, layoutParams.height); + if (mPreferences != null) { + mPreferences.setWindowWidth(layoutParams.width); + mPreferences.setWindowHeight(layoutParams.height); + } return true; } @@ -86,9 +121,39 @@ private static int computeLayoutFlags(boolean withFocus) { } } - public void initializeFloatingWindow() { + public void initFloatView(TermuxFloatService service) { + Logger.logDebug(LOG_TAG, "initFloatView"); + + // Load termux shared properties + mProperties = new TermuxFloatAppSharedProperties(getContext()); + + // Load termux float shared preferences + // This will also fail if TermuxConstants.TERMUX_FLOAT_PACKAGE_NAME does not equal applicationId + mPreferences = TermuxFloatAppSharedPreferences.build(getContext(), true); + if (mPreferences == null) { + return; + } + + mTermuxFloatSessionClient = new TermuxFloatSessionClient(service, this); + mTerminalView = findViewById(R.id.terminal_view); - mTerminalView.setOnKeyListener(new TermuxFloatViewClient(this)); + mTermuxFloatViewClient = new TermuxFloatViewClient(this, mTermuxFloatSessionClient); + mTerminalView.setTerminalViewClient(mTermuxFloatViewClient); + mTermuxFloatViewClient.initFloatView(); + + mFloatingBubbleManager = new FloatingBubbleManager(this); + initWindowControls(); + } + + private void initWindowControls() { + mWindowControls = findViewById(R.id.window_controls); + mWindowControls.setOnClickListener(v -> changeFocus(true)); + + Button minimizeButton = findViewById(R.id.minimize_button); + minimizeButton.setOnClickListener(v -> mFloatingBubbleManager.toggleBubble()); + + Button exitButton = findViewById(R.id.exit_button); + exitButton.setOnClickListener(v -> exit()); } @Override @@ -100,17 +165,16 @@ protected void onAttachedToWindow() { DISPLAY_WIDTH = displaySize.x; DISPLAY_HEIGHT = displaySize.y; - checkForFont(); + if (mTermuxFloatSessionClient != null) + mTermuxFloatSessionClient.onAttachedToWindow(); } - void checkForFont() { - try { - @SuppressLint("SdCardPath") File fontFile = new File("/data/data/com.termux/files/home/.termux/font.ttf"); - final Typeface newTypeface = (fontFile.exists() && fontFile.length() > 0) ? Typeface.createFromFile(fontFile) : Typeface.MONOSPACE; - mTerminalView.setTypeface(newTypeface); - } catch (Exception e) { - Log.e(EmulatorDebug.LOG_TAG, "Error in checkForFont()", e); - } + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + if (mTermuxFloatSessionClient != null) + mTermuxFloatSessionClient.onDetachedFromWindow(); } @SuppressLint("RtlHardcoded") @@ -127,11 +191,17 @@ public void launchFloatingWindow() { layoutParams.format = PixelFormat.RGBA_8888; layoutParams.gravity = Gravity.TOP | Gravity.LEFT; - TermuxFloatPrefs.applySavedGeometry(getContext(), layoutParams); + + if (mPreferences != null) { + layoutParams.x = mPreferences.getWindowX(); + layoutParams.y = mPreferences.getWindowY(); + layoutParams.width = mPreferences.getWindowWidth(); + layoutParams.height = mPreferences.getWindowHeight(); + } mWindowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); - mWindowManager.addView(this, layoutParams); - imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (getWindowToken() == null) + mWindowManager.addView(this, layoutParams); showTouchKeyboard(); } @@ -147,6 +217,13 @@ public boolean onInterceptTouchEvent(MotionEvent event) { int y = location[1]; float touchX = event.getRawX(); float touchY = event.getRawY(); + + if (didClickInsideWindowControls(touchX, touchY)) { + // avoid unintended focus event if we are tapping on our window controls + // so that keyboard doesn't possibly show briefly + return false; + } + boolean clickedInside = (touchX >= x) && (touchX <= (x + layoutParams.width)) && (touchY >= y) && (touchY <= (y + layoutParams.height)); switch (event.getAction()) { @@ -163,24 +240,33 @@ public boolean onInterceptTouchEvent(MotionEvent event) { return false; } + private boolean didClickInsideWindowControls(float touchX, float touchY) { + if (mWindowControls.getVisibility() == View.GONE) { + return false; + } + mWindowControls.getLocationOnScreen(windowControlsLocation); + int controlsX = windowControlsLocation[0]; + int controlsY = windowControlsLocation[1]; + + return (touchX >= controlsX && touchX <= controlsX + mWindowControls.getWidth()) && + (touchY >= controlsY && touchY <= controlsY + mWindowControls.getHeight()); + } + void showTouchKeyboard() { - mTerminalView.post(new Runnable() { - @Override - public void run() { - imm.showSoftInput(mTerminalView, InputMethodManager.SHOW_IMPLICIT); - } - }); + mTerminalView.post(() -> KeyboardUtils.showSoftKeyboard(getContext(), mTerminalView)); + + } + + void hideTouchKeyboard() { + mTerminalView.post(() -> KeyboardUtils.hideSoftKeyboard(getContext(), mTerminalView)); } void updateLongPressMode(boolean newValue) { isInLongPressState = newValue; - setBackgroundResource(newValue ? R.drawable.floating_window_background_resize : R.drawable.floating_window_background); + mFloatingBubbleManager.updateLongPressBackgroundResource(isInLongPressState); setAlpha(newValue ? ALPHA_MOVING : (withFocus ? ALPHA_FOCUS : ALPHA_NOT_FOCUS)); - if (newValue) { - Toast toast = Toast.makeText(getContext(), R.string.after_long_press, Toast.LENGTH_SHORT); - toast.setGravity(Gravity.CENTER, 0, 0); - toast.show(); - } + if (newValue && !mFloatingBubbleManager.isMinimized()) + Logger.showToast(getContext(), getContext().getString(R.string.after_long_press), false); } /** @@ -197,7 +283,10 @@ public boolean onTouchEvent(MotionEvent event) { layoutParams.x = Math.min(DISPLAY_WIDTH - layoutParams.width, Math.max(0, initialX + (int) (event.getRawX() - initialTouchX))); layoutParams.y = Math.min(DISPLAY_HEIGHT - layoutParams.height, Math.max(0, initialY + (int) (event.getRawY() - initialTouchY))); mWindowManager.updateViewLayout(TermuxFloatView.this, layoutParams); - TermuxFloatPrefs.saveWindowPosition(getContext(), layoutParams.x, layoutParams.y); + if (mPreferences != null) { + mPreferences.setWindowX(layoutParams.x); + mPreferences.setWindowY(layoutParams.y); + } break; case MotionEvent.ACTION_UP: updateLongPressMode(false); @@ -212,18 +301,63 @@ public boolean onTouchEvent(MotionEvent event) { * Visually indicate focus and show the soft input as needed. */ void changeFocus(boolean newFocus) { + if (newFocus && mFloatingBubbleManager.isMinimized()) { + mFloatingBubbleManager.displayAsFloatingWindow(); + } if (newFocus == withFocus) { if (newFocus) showTouchKeyboard(); return; } withFocus = newFocus; layoutParams.flags = computeLayoutFlags(withFocus); - mWindowManager.updateViewLayout(this, layoutParams); + if (getWindowToken() != null) + mWindowManager.updateViewLayout(this, layoutParams); setAlpha(newFocus ? ALPHA_FOCUS : ALPHA_NOT_FOCUS); } public void closeFloatingWindow() { - mWindowManager.removeView(this); + if (getWindowToken() != null) + mWindowManager.removeView(this); + + mFloatingBubbleManager.cleanup(); + mFloatingBubbleManager = null; + } + + private void exit() { + Intent exitIntent = new Intent(getContext(), TermuxFloatService.class).setAction(TermuxConstants.TERMUX_FLOAT_APP.TERMUX_FLOAT_SERVICE.ACTION_STOP_SERVICE); + getContext().startService(exitIntent); } + + + public boolean isVisible() { + return isAttachedToWindow() && isShown(); + } + + public TerminalView getTerminalView() { + return mTerminalView; + } + + public TermuxFloatViewClient getTermuxFloatViewClient() { + return mTermuxFloatViewClient; + } + + public TermuxFloatSessionClient getTermuxFloatSessionClient() { + return mTermuxFloatSessionClient; + } + + public TermuxFloatAppSharedPreferences getPreferences() { + return mPreferences; + } + + public TermuxFloatAppSharedProperties getProperties() { + return mProperties; + } + + + public void reloadViewStyling() { + // Leaving here for future support for termux-reload-settings + if (mTermuxFloatSessionClient != null) + mTermuxFloatSessionClient.onReload(); + } } diff --git a/app/src/main/java/com/termux/window/TermuxFloatViewClient.java b/app/src/main/java/com/termux/window/TermuxFloatViewClient.java index b531fdd..ad814ed 100644 --- a/app/src/main/java/com/termux/window/TermuxFloatViewClient.java +++ b/app/src/main/java/com/termux/window/TermuxFloatViewClient.java @@ -5,30 +5,55 @@ import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; -import android.view.inputmethod.InputMethodManager; +import com.termux.shared.termux.terminal.TermuxTerminalViewClientBase; +import com.termux.shared.view.KeyboardUtils; import com.termux.terminal.KeyHandler; import com.termux.terminal.TerminalEmulator; import com.termux.terminal.TerminalSession; -import com.termux.view.TerminalViewClient; -public class TermuxFloatViewClient implements TerminalViewClient { +public class TermuxFloatViewClient extends TermuxTerminalViewClientBase { + + private final TermuxFloatView mView; + private final TermuxFloatSessionClient mTermuxFloatSessionClient; - private final TermuxFloatView view; /** * Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */ boolean mVirtualControlKeyDown, mVirtualFnKeyDown; - public TermuxFloatViewClient(TermuxFloatView view) { - this.view = view; + public TermuxFloatViewClient(TermuxFloatView view, TermuxFloatSessionClient termuxFloatSessionClient) { + mView = view; + mTermuxFloatSessionClient = termuxFloatSessionClient; + } + + /** + * Should be called when TermuxFloatView.initFloatView() is called + */ + public void initFloatView() { + mView.getTerminalView().setTextSize(mView.getPreferences().getFontSize()); + + // Set {@link TerminalView#TERMINAL_VIEW_KEY_LOGGING_ENABLED} value + boolean isTerminalViewKeyLoggingEnabled = mView.getPreferences().isTerminalViewKeyLoggingEnabled(true); + mView.getTerminalView().setIsTerminalViewKeyLoggingEnabled(isTerminalViewKeyLoggingEnabled); } + /** + * Should be called when {@link com.termux.view.TerminalView#mEmulator} is set + */ + @Override + public void onEmulatorSet() { + // This is being called every time float bubble is maximized + mTermuxFloatSessionClient.checkForFontAndColors(); + } + + + @Override public float onScale(float scale) { if (scale < 0.9f || scale > 1.1f) { boolean increase = scale > 1.f; - ((TermuxFloatService) view.getContext()).changeFontSize(increase); + changeFontSize(increase); return 1.0f; } return scale; @@ -36,23 +61,28 @@ public float onScale(float scale) { @Override public boolean onLongPress(MotionEvent event) { - view.updateLongPressMode(true); - view.getLocationOnScreen(view.location); - view.initialX = view.location[0]; - view.initialY = view.location[1]; - view.initialTouchX = event.getRawX(); - view.initialTouchY = event.getRawY(); + mView.updateLongPressMode(true); + mView.getLocationOnScreen(mView.location); + mView.initialX = mView.location[0]; + mView.initialY = mView.location[1]; + mView.initialTouchX = event.getRawX(); + mView.initialTouchY = event.getRawY(); return true; } @Override - public void onSingleTapUp(MotionEvent e) { - // Do nothing. + public boolean shouldBackButtonBeMappedToEscape() { + return mView.getProperties().isBackKeyTheEscapeKey(); } @Override - public boolean shouldBackButtonBeMappedToEscape() { - return false; + public boolean shouldEnforceCharBasedInput() { + return mView.getProperties().isEnforcingCharBasedInput(); + } + + @Override + public boolean shouldUseCtrlSpaceWorkaround() { + return mView.getProperties().isUsingCtrlSpaceWorkaround(); } @Override @@ -64,7 +94,8 @@ public void copyModeChanged(boolean copyMode) { public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession session) { if (handleVirtualKeys(keyCode, e, true)) return true; - if (e.isCtrlPressed() && e.isAltPressed()) { + if (!mView.getProperties().areHardwareKeyboardShortcutsDisabled() && + e.isCtrlPressed() && e.isAltPressed()) { // Get the unmodified code point: int unicodeChar = e.getUnicodeChar(0); @@ -73,8 +104,13 @@ public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession session) { } else if (unicodeChar == 'f'/* full screen */) { // TODO: Toggle full screen. } else if (unicodeChar == 'k'/* keyboard */) { - InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); + KeyboardUtils.toggleSoftKeyboard(mView.getContext()); + } else if (unicodeChar == '+' || e.getUnicodeChar(KeyEvent.META_SHIFT_ON) == '+') { + // We also check for the shifted char here since shift may be required to produce '+', + // see https://github.com/termux/termux-api/issues/2 + changeFontSize(true); + } else if (unicodeChar == '-') { + changeFontSize(false); } return true; } @@ -181,7 +217,7 @@ public boolean onCodePoint(int codePoint, boolean ctrlDown, TerminalSession sess // Volume control. case 'v': resultingCodePoint = -1; - AudioManager audio = (AudioManager) view.getContext().getSystemService(Context.AUDIO_SERVICE); + AudioManager audio = (AudioManager) mView.getContext().getSystemService(Context.AUDIO_SERVICE); audio.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME, AudioManager.USE_DEFAULT_STREAM_TYPE, AudioManager.FLAG_SHOW_UI); break; } @@ -203,7 +239,9 @@ public boolean onCodePoint(int codePoint, boolean ctrlDown, TerminalSession sess */ private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) { InputDevice inputDevice = event.getDevice(); - if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) { + if (mView.getProperties().areVirtualVolumeKeysDisabled()) { + return false; + } else if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) { // Do not steal dedicated buttons from a full external keyboard. return false; } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { @@ -216,4 +254,10 @@ private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) { return false; } + + + public void changeFontSize(boolean increase) { + mView.getPreferences().changeFontSize(increase); + mView.getTerminalView().setTextSize(mView.getPreferences().getFontSize()); + } } diff --git a/app/src/main/java/com/termux/window/settings/properties/TermuxFloatAppSharedProperties.java b/app/src/main/java/com/termux/window/settings/properties/TermuxFloatAppSharedProperties.java new file mode 100644 index 0000000..0986af8 --- /dev/null +++ b/app/src/main/java/com/termux/window/settings/properties/TermuxFloatAppSharedProperties.java @@ -0,0 +1,22 @@ +package com.termux.window.settings.properties; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import com.termux.shared.termux.TermuxConstants; +import com.termux.shared.termux.settings.properties.TermuxPropertyConstants; +import com.termux.shared.termux.settings.properties.TermuxSharedProperties; + +public class TermuxFloatAppSharedProperties extends TermuxSharedProperties { + + private static final String LOG_TAG = "TermuxFloatAppSharedProperties"; + + public TermuxFloatAppSharedProperties(@NonNull Context context) { + super(context, TermuxConstants.TERMUX_FLOAT_APP_NAME, + TermuxConstants.TERMUX_FLOAT_PROPERTIES_FILE_PATHS_LIST, + TermuxPropertyConstants.TERMUX_APP_PROPERTIES_LIST, + new SharedPropertiesParserClient()); + } + +} diff --git a/app/src/main/res/drawable-anydpi-v26/ic_launcher.xml b/app/src/main/res/drawable-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6192469 --- /dev/null +++ b/app/src/main/res/drawable-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_exit_icon.xml b/app/src/main/res/drawable/ic_exit_icon.xml new file mode 100644 index 0000000..12f9875 --- /dev/null +++ b/app/src/main/res/drawable/ic_exit_icon.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_foreground.xml b/app/src/main/res/drawable/ic_foreground.xml new file mode 100644 index 0000000..3f3e59b --- /dev/null +++ b/app/src/main/res/drawable/ic_foreground.xml @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher.xml b/app/src/main/res/drawable/ic_launcher.xml new file mode 100644 index 0000000..749a55b --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher.xml @@ -0,0 +1,34 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_minimize_icon.xml b/app/src/main/res/drawable/ic_minimize_icon.xml new file mode 100644 index 0000000..918d3a3 --- /dev/null +++ b/app/src/main/res/drawable/ic_minimize_icon.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/round_button_with_outline.xml b/app/src/main/res/drawable/round_button_with_outline.xml new file mode 100644 index 0000000..79b8df4 --- /dev/null +++ b/app/src/main/res/drawable/round_button_with_outline.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 8acbdd3..65b0b10 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -4,8 +4,38 @@ android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="@drawable/floating_window_background" + android:orientation="vertical" android:padding="1px" > + +