WIP: Android 10+ as target and Google Play distribution support

This commit is contained in:
Fredrik Fornwall 2023-10-08 22:13:48 +02:00
parent e2f0edf4d2
commit aa480474b3
No known key found for this signature in database
249 changed files with 2949 additions and 32830 deletions

View File

@ -4,6 +4,7 @@ on:
push:
branches:
- master
- google-play
pull_request:
branches:
- master
@ -11,19 +12,18 @@ on:
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
package_variant: [ apt-android-7, apt-android-5 ]
steps:
- name: Clone repository
uses: actions/checkout@v4
- name: Setup java 17 as required by gradle
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- name: Build APKs
shell: bash {0}
env:
PACKAGE_VARIANT: ${{ matrix.package_variant }}
run: |
exit_on_error() { echo "$1"; exit 1; }
@ -42,7 +42,7 @@ jobs:
fi
APK_DIR_PATH="./app/build/outputs/apk/debug"
APK_VERSION_TAG="$RELEASE_VERSION_NAME-${{ env.PACKAGE_VARIANT }}-github-debug" # Note the "-", GITHUB_SHA will already have "+" before it
APK_VERSION_TAG="$RELEASE_VERSION_NAME-github-debug" # Note the "-", GITHUB_SHA will already have "+" before it
APK_BASENAME_PREFIX="termux-app_$APK_VERSION_TAG"
# Used by attachment steps later
@ -53,7 +53,6 @@ jobs:
echo "Building APKs for 'APK_VERSION_TAG' build"
export TERMUX_APP_VERSION_NAME="${RELEASE_VERSION_NAME/v/}" # Used by app/build.gradle
export TERMUX_APK_VERSION_TAG="$APK_VERSION_TAG" # Used by app/build.gradle
export TERMUX_PACKAGE_VARIANT="${{ env.PACKAGE_VARIANT }}" # Used by app/build.gradle
if ! ./gradlew assembleDebug; then
exit_on_error "Build failed for '$APK_VERSION_TAG' build."
fi

View File

@ -16,6 +16,11 @@ jobs:
steps:
- name: Clone repository
uses: actions/checkout@v4
- name: Setup java 17 as required by gradle
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- name: Execute tests
run: |
./gradlew test

View File

@ -3,4 +3,3 @@ The `termux/termux-app` repository is released under [GPLv3 only](https://www.gn
### Exceptions
- [Terminal Emulator for Android](https://github.com/jackpal/Android-Terminal-Emulator) code is used which is released under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) license. Check [`terminal-view`](terminal-view) and [`terminal-emulator`](terminal-emulator) libraries.
- Check [`termux-shared/LICENSE.md`](termux-shared/LICENSE.md) for `termux-shared` library related exceptions.

View File

@ -252,10 +252,6 @@ Users must post complete report (optionally without sensitive info) when reporti
## For Maintainers and Contributors
The [termux-shared](termux-shared) library was added in [`v0.109`](https://github.com/termux/termux-app/releases/tag/v0.109). It defines shared constants and utils of the Termux app and its plugins. It was created to allow for the removal of all hardcoded paths in the Termux app. Some of the termux plugins are using this as well and rest will in future. If you are contributing code that is using a constant or a util that may be shared, then define it in `termux-shared` library if it currently doesn't exist and reference it from there. Update the relevant changelogs as well. Pull requests using hardcoded values **will/should not** be accepted. Termux app and plugin specific classes must be added under `com.termux.shared.termux` package and general classes outside it. The [`termux-shared` `LICENSE`](termux-shared/LICENSE.md) must also be checked and updated if necessary when contributing code. The licenses of any external library or code must be honoured.
The main Termux constants are defined by [`TermuxConstants`](https://github.com/termux/termux-app/blob/master/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java) class. It also contains information on how to fork Termux or build it with your own package name. Changing the package name will require building the bootstrap zip packages and other packages with the new `$PREFIX`, check [Building Packages](https://github.com/termux/termux-packages/wiki/Building-packages) for more info.
Check [Termux Libraries](https://github.com/termux/termux-app/wiki/Termux-Libraries) for how to import termux libraries in plugin apps and [Forking and Local Development](https://github.com/termux/termux-app/wiki/Termux-Libraries#forking-and-local-development) for how to update termux libraries for plugins.
Commit messages **must** use [Conventional Commits](https://www.conventionalcommits.org) specs so that chagelogs can automatically be generated by the [`create-conventional-changelog`](https://github.com/termux/create-conventional-changelog) script, check its repo for further details on the spec. Use the following `types` as `Added: Add foo`, `Added|Fixed: Add foo and fix bar`, `Changed!: Change baz as a breaking change`, etc. You can optionally add a scope as well, like `Fixed(terminal): Some bug`. The space after `:` is necessary.
@ -271,13 +267,3 @@ Commit messages **must** use [Conventional Commits](https://www.conventionalcomm
Changelogs for releases are generated based on [Keep a Changelog](https://github.com/olivierlacan/keep-a-changelog) specs.
The `versionName` in `build.gradle` files of Termux and its plugin apps must follow the [semantic version `2.0.0` spec](https://semver.org/spec/v2.0.0.html) in the format `major.minor.patch(-prerelease)(+buildmetadata)`. When bumping `versionName` in `build.gradle` files and when creating a tag for new releases on GitHub, make sure to include the patch number as well, like `v0.1.0` instead of just `v0.1`. The `build.gradle` files and `attach_debug_apks_to_release` workflow validates the version as well and the build/attachment will fail if `versionName` does not follow the spec.
##
## Forking
- Check [`TermuxConstants`](https://github.com/termux/termux-app/blob/master/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java) javadocs for instructions on what changes to make in the app to change package name.
- You also need to recompile bootstrap zip for the new package name. Check [building bootstrap](https://github.com/termux/termux-packages/wiki/For-maintainers#build-bootstrap-archives), [here](https://github.com/termux/termux-app/issues/1983) and [here](https://github.com/termux/termux-app/issues/2081#issuecomment-865280111).
- Currently, not all plugins use `TermuxConstants` from `termux-shared` library and have hardcoded `com.termux` values and will need to be manually patched.
- If forking termux plugins, check [Forking and Local Development](https://github.com/termux/termux-app/wiki/Termux-Libraries#forking-and-local-development) for info on how to use termux libraries for plugins.

View File

@ -2,18 +2,9 @@ plugins {
id "com.android.application"
}
ext {
// The packageVariant defines the bootstrap variant that will be included in the app APK.
// This must be supported by com.termux.shared.termux.TermuxBootstrap.PackageVariant or app will
// crash at startup.
// Bootstrap of a different variant must not be manually installed by the user after app installation
// by replacing $PREFIX since app code is dependant on the variant used to build the APK.
// Currently supported values are: [ "apt-android-7" "apt-android-5" ]
packageVariant = System.getenv("TERMUX_PACKAGE_VARIANT") ?: "apt-android-7" // Default: "apt-android-7"
}
android {
compileSdkVersion project.properties.compileSdkVersion.toInteger()
namespace "com.termux"
ndkVersion = System.getenv("JITPACK_NDK_VERSION") ?: project.properties.ndkVersion
def appVersionName = System.getenv("TERMUX_APP_VERSION_NAME") ?: ""
def apkVersionTag = System.getenv("TERMUX_APK_VERSION_TAG") ?: ""
@ -21,34 +12,25 @@ android {
def splitAPKsForReleaseBuilds = System.getenv("TERMUX_SPLIT_APKS_FOR_RELEASE_BUILDS") ?: "0" // F-Droid does not support split APKs #1904
dependencies {
implementation "androidx.annotation:annotation:1.3.0"
implementation "androidx.core:core:1.6.0"
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
implementation "androidx.preference:preference:1.1.1"
implementation "androidx.annotation:annotation:1.7.0"
implementation "androidx.core:core:1.12.0"
implementation "androidx.drawerlayout:drawerlayout:1.2.0"
implementation "androidx.viewpager:viewpager:1.0.0"
implementation "com.google.android.material:material:1.4.0"
implementation "com.google.guava:guava:24.1-jre"
implementation "io.noties.markwon:core:$markwonVersion"
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
implementation "io.noties.markwon:linkify:$markwonVersion"
implementation "io.noties.markwon:recycler:$markwonVersion"
implementation "com.google.android.material:material:1.10.0"
implementation project(":terminal-view")
implementation project(":termux-shared")
}
defaultConfig {
applicationId "com.termux"
minSdkVersion project.properties.minSdkVersion.toInteger()
targetSdkVersion project.properties.targetSdkVersion.toInteger()
compileSdk project.properties.compileSdkVersion.toInteger()
versionCode 118
versionName "0.118.0"
if (appVersionName) versionName = appVersionName
validateVersionName(versionName)
buildConfigField "String", "TERMUX_PACKAGE_VARIANT", "\"" + project.ext.packageVariant + "\"" // Used by TermuxApplication class
manifestPlaceholders.TERMUX_PACKAGE_NAME = "com.termux"
manifestPlaceholders.TERMUX_APP_NAME = "Termux"
manifestPlaceholders.TERMUX_API_APP_NAME = "Termux:API"
@ -97,11 +79,8 @@ android {
}
compileOptions {
// Flag to enable support for the new language APIs
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
externalNativeBuild {
@ -110,7 +89,7 @@ android {
}
}
lintOptions {
lint {
disable 'ProtectedPermissions'
}
@ -130,10 +109,10 @@ android {
variant.outputs.all { output ->
if (variant.buildType.name == "debug") {
def abi = output.getFilter(com.android.build.OutputFile.ABI)
outputFileName = new File("termux-app_" + (apkVersionTag ? apkVersionTag : project.ext.packageVariant + "-" + "debug") + "_" + (abi ? abi : "universal") + ".apk")
outputFileName = new File("termux-app_" + (apkVersionTag ? apkVersionTag : "-" + "debug") + "_" + (abi ? abi : "universal") + ".apk")
} else if (variant.buildType.name == "release") {
def abi = output.getFilter(com.android.build.OutputFile.ABI)
outputFileName = new File("termux-app_" + (apkVersionTag ? apkVersionTag : project.ext.packageVariant + "-" + "release") + "_" + (abi ? abi : "universal") + ".apk")
outputFileName = new File("termux-app_" + (apkVersionTag ? apkVersionTag : "-" + "release") + "_" + (abi ? abi : "universal") + ".apk")
}
}
}
@ -143,7 +122,6 @@ android {
dependencies {
testImplementation "junit:junit:4.13.2"
testImplementation "org.robolectric:robolectric:4.10"
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.1.5"
}
task versionName {
@ -182,7 +160,8 @@ def downloadBootstrap(String arch, String expectedChecksum, String version) {
}
}
def remoteUrl = "https://github.com/termux/termux-packages/releases/download/bootstrap-" + version + "/bootstrap-" + arch + ".zip"
// def remoteUrl = "https://github.com/termux/termux-packages/releases/download/bootstrap-" + version + "/bootstrap-" + arch + ".zip"
def remoteUrl = "https://fornwall.me/tmp/bootstrap-" + arch + "-test-v" + version + ".zip"
logger.quiet("Downloading " + remoteUrl + " ...")
file.parentFile.mkdirs()
@ -212,22 +191,11 @@ clean {
task downloadBootstraps() {
doLast {
def packageVariant = project.ext.packageVariant
if (packageVariant == "apt-android-7") {
def version = "2022.04.28-r5" + "+" + packageVariant
downloadBootstrap("aarch64", "4a51a7eb209fe82efc24d52e3cccc13165f27377290687cb82038cbd8e948430", version)
downloadBootstrap("arm", "6459a786acbae50d4c8a36fa1c3de6a4dd2d482572f6d54f73274709bd627325", version)
downloadBootstrap("i686", "919d212b2f19e08600938db4079e794e947365022dbfd50ac342c50fcedcd7be", version)
downloadBootstrap("x86_64", "61b02fdc03ea4f5d9da8d8cf018013fdc6659e6da6cbf44e9b24d1c623580b89", version)
} else if (packageVariant == "apt-android-5") {
def version = "2022.04.28-r6" + "+" + packageVariant
downloadBootstrap("aarch64", "913609d439415c828c5640be1b0561467e539cb1c7080662decaaca2fb4820e7", version)
downloadBootstrap("arm", "26bfb45304c946170db69108e5eb6e3641aad751406ce106c80df80cad2eccf8", version)
downloadBootstrap("i686", "46dcfeb5eef67ba765498db9fe4c50dc4690805139aa0dd141a9d8ee0693cd27", version)
downloadBootstrap("x86_64", "615b590679ee6cd885b7fd2ff9473c845e920f9b422f790bb158c63fe42b8481", version)
} else {
throw new GradleException("Unsupported TERMUX_PACKAGE_VARIANT \"" + packageVariant + "\"")
}
def version = "3"
downloadBootstrap("aarch64", "308484efc4400a003a731836f6c33dfa6e5fc04abc27a2268968a3e5f549114b", version)
downloadBootstrap("arm", "0b39a9d53882fb7878cd5b2cf5846e50853e69ebf7df283afcf8ea51af14b322", version)
downloadBootstrap("i686", "ec7a18f5fa17d01cc0aa8e1389b21f3d9b99c8eddd33d4ddde36dd2380f74a07", version)
downloadBootstrap("x86_64", "e7d90df0dcb698c2413c5581b238d425e59fc84e178358a9e56b2101be341279", version)
}
}
@ -236,3 +204,8 @@ afterEvaluate {
variant.javaCompileProvider.get().dependsOn(downloadBootstraps)
}
}
// https://stackoverflow.com/questions/75274720/a-failure-occurred-while-executing-appcheckdebugduplicateclasses/
configurations.implementation {
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8'
}

View File

@ -10,8 +10,3 @@
-dontobfuscate
#-renamesourcefileattribute SourceFile
#-keepattributes SourceFile,LineNumberTable
# Temp fix for androidx.window:window:1.0.0-alpha09 imported by termux-shared
# https://issuetracker.google.com/issues/189001730
# https://android-review.googlesource.com/c/platform/frameworks/support/+/1757630
-keep class androidx.window.** { *; }

View File

@ -1,9 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.termux"
android:installLocation="internalOnly"
android:sharedUserId="${TERMUX_PACKAGE_NAME}"
android:sharedUserLabel="@string/shared_user_label">
<uses-feature
@ -20,109 +18,74 @@
android:label="@string/permission_run_command_label"
android:protectionLevel="dangerous" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<permission
android:name="com.termux.permission.internal"
android:description="@string/permission_run_command_description"
android:icon="@mipmap/ic_launcher"
android:label="@string/permission_run_command_label"
android:protectionLevel="signature" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<!-- See https://developer.android.com/guide/components/foreground-services -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.READ_LOGS" />
<uses-permission android:name="android.permission.DUMP" />
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- Perhaps to check phantom?
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" tools:ignore="ProtectedPermissions" />
-->
<application
android:name=".app.TermuxApplication"
android:allowBackup="false"
android:banner="@drawable/banner"
android:extractNativeLibs="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/application_name"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="false"
android:theme="@style/Theme.TermuxApp.DayNight.DarkActionBar"
tools:targetApi="m">
android:theme="@style/Theme.TermuxActivity.DayNight.NoActionBar">
<activity
android:name=".app.TermuxActivity"
android:exported="true"
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|keyboard|keyboardHidden|navigation"
android:label="@string/application_name"
android:launchMode="singleTask"
android:resizeableActivity="true"
android:theme="@style/Theme.TermuxActivity.DayNight.NoActionBar"
tools:targetApi="n">
android:theme="@style/Theme.TermuxActivity.DayNight.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity-alias
android:name=".HomeActivity"
android:exported="true"
android:targetActivity=".app.TermuxActivity">
<!-- Launch activity automatically on boot on Android Things devices -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.IOT_LAUNCHER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity-alias>
android:name=".app.TermuxActivityInternal"
android:targetActivity=".app.TermuxActivity"/>
<activity
android:name=".app.activities.HelpActivity"
android:name=".app.TermuxHelpActivity"
android:exported="false"
android:label="@string/application_name"
android:parentActivityName=".app.TermuxActivity"
android:resizeableActivity="true"
tools:targetApi="n" />
<activity
android:name=".app.activities.SettingsActivity"
android:exported="true"
android:label="@string/title_activity_termux_settings"
android:theme="@style/Theme.TermuxApp.DayNight.NoActionBar" />
<activity
android:name=".shared.activities.ReportActivity"
android:theme="@style/Theme.MarkdownViewActivity.DayNight"
android:documentLaunchMode="intoExisting" />
<activity
android:name=".app.api.file.FileReceiverActivity"
android:name=".app.TermuxFileReceiverActivity"
android:excludeFromRecents="true"
android:exported="false"
android:exported="true"
android:noHistory="true"
android:resizeableActivity="true"
android:taskAffinity="${TERMUX_PACKAGE_NAME}.filereceiver"
tools:targetApi="n">
</activity>
<activity-alias
android:name=".app.api.file.FileShareReceiverActivity"
android:exported="true"
android:targetActivity=".app.api.file.FileReceiverActivity">
android:taskAffinity="${TERMUX_PACKAGE_NAME}.filereceiver" >
<!-- Accept multiple file types when sending. -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
@ -137,12 +100,6 @@
<data android:mimeType="text/*" />
<data android:mimeType="video/*" />
</intent-filter>
</activity-alias>
<activity-alias
android:name=".app.api.file.FileViewReceiverActivity"
android:exported="true"
android:targetActivity=".app.api.file.FileReceiverActivity">
<!-- Accept multiple file types to let Termux be usable as generic file viewer. -->
<intent-filter tools:ignore="AppLinkUrlError">
@ -156,10 +113,10 @@
<data android:mimeType="text/*" />
<data android:mimeType="video/*" />
</intent-filter>
</activity-alias>
</activity>
<provider
android:name=".filepicker.TermuxDocumentsProvider"
android:name=".app.TermuxDocumentsProvider"
android:authorities="${TERMUX_PACKAGE_NAME}.documents"
android:exported="true"
android:grantUriPermissions="true"
@ -176,57 +133,41 @@
android:grantUriPermissions="true"
android:permission="${TERMUX_PACKAGE_NAME}.permission.RUN_COMMAND" />
<receiver
android:name=".app.TermuxOpenReceiver"
android:exported="false" />
<receiver
android:name=".app.event.SystemEventReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver
android:name=".shared.activities.ReportActivity$ReportActivityBroadcastReceiver"
android:exported="false" />
<service
android:name=".app.TermuxService"
android:exported="false" />
<service
android:name=".app.RunCommandService"
android:exported="true"
android:permission="${TERMUX_PACKAGE_NAME}.permission.RUN_COMMAND">
<intent-filter>
<action android:name="${TERMUX_PACKAGE_NAME}.RUN_COMMAND" />
</intent-filter>
android:foregroundServiceType="specialUse"
android:permission=""
android:exported="false">
<!--
About android:foregroundServiceType: Starting with Android 14 foreground
services must define a service type. See:
https://developer.android.com/guide/components/fg-service-types
Termux uses the "Special Use" type:
https://developer.android.com/guide/components/fg-service-types#special-use
-->
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="explanation_for_special_use"/>
</service>
<!-- This (or rather, value 2.1 or higher) is needed to make the Samsung Galaxy S8 mark the
app with "This app is optimized to run in full screen." -->
<meta-data
android:name="android.max_aspect"
android:value="10.0" />
<!-- https://developer.samsung.com/samsung-dex/modify-optimizing.html -->
<!-- Version < 3.0. DeX Mode and Screen Mirroring support -->
<meta-data
android:name="com.samsung.android.keepalive.density"
android:value="true" />
<!-- Version >= 3.0. DeX Dual Mode support -->
<meta-data
android:name="com.samsung.android.multidisplay.keep_process_alive"
android:value="true" />
<meta-data
android:name="com.sec.android.support.multiwindow"
android:value="true" />

View File

@ -1,287 +0,0 @@
package com.termux.app;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Intent;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import com.termux.R;
import com.termux.shared.data.DataUtils;
import com.termux.shared.data.IntentUtils;
import com.termux.shared.termux.plugins.TermuxPluginUtils;
import com.termux.shared.termux.file.TermuxFileUtils;
import com.termux.shared.file.filesystem.FileType;
import com.termux.shared.errors.Errno;
import com.termux.shared.errors.Error;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
import com.termux.shared.file.FileUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.notification.NotificationUtils;
import com.termux.shared.shell.command.ExecutionCommand;
import com.termux.shared.shell.command.ExecutionCommand.Runner;
/**
* A service that receives {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent from third party apps and
* plugins that contains info on command execution and forwards the extras to {@link TermuxService}
* for the actual execution.
*
* Check https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent for more info.
*/
public class RunCommandService extends Service {
private static final String LOG_TAG = "RunCommandService";
class LocalBinder extends Binder {
public final RunCommandService service = RunCommandService.this;
}
private final IBinder mBinder = new RunCommandService.LocalBinder();
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
@Override
public void onCreate() {
Logger.logVerbose(LOG_TAG, "onCreate");
runStartForeground();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Logger.logDebug(LOG_TAG, "onStartCommand");
if (intent == null) return Service.START_NOT_STICKY;
// Run again in case service is already started and onCreate() is not called
runStartForeground();
Logger.logVerboseExtended(LOG_TAG, "Intent Received:\n" + IntentUtils.getIntentString(intent));
ExecutionCommand executionCommand = new ExecutionCommand();
executionCommand.pluginAPIHelp = this.getString(R.string.error_run_command_service_api_help, RUN_COMMAND_SERVICE.RUN_COMMAND_API_HELP_URL);
Error error;
String errmsg;
// If invalid action passed, then just return
if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) {
errmsg = this.getString(R.string.error_run_command_service_invalid_intent_action, intent.getAction());
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return stopService();
}
String executableExtra = executionCommand.executable = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH, null);
executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS, null);
/*
* If intent was sent with `am` command, then normal comma characters may have been replaced
* with alternate characters if a normal comma existed in an argument itself to prevent it
* splitting into multiple arguments by `am` command.
* If `tudo` or `sudo` are used, then simply using their `-r` and `--comma-alternative` command
* options can be used without passing the below extras, but native supports is helpful if
* they are not being used.
* https://github.com/agnostic-apollo/tudo#passing-arguments-using-run_command-intent
* https://android.googlesource.com/platform/frameworks/base/+/21bdaf1/cmds/am/src/com/android/commands/am/Am.java#572
*/
boolean replaceCommaAlternativeCharsInArguments = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS, false);
if (replaceCommaAlternativeCharsInArguments) {
String commaAlternativeCharsInArguments = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS, null);
if (commaAlternativeCharsInArguments == null)
commaAlternativeCharsInArguments = TermuxConstants.COMMA_ALTERNATIVE;
// Replace any commaAlternativeCharsInArguments characters with normal commas
DataUtils.replaceSubStringsInStringArrayItems(executionCommand.arguments, commaAlternativeCharsInArguments, TermuxConstants.COMMA_NORMAL);
}
executionCommand.stdin = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_STDIN, null);
executionCommand.workingDirectory = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_WORKDIR, null);
// If EXTRA_RUNNER is passed, use that, otherwise check EXTRA_BACKGROUND and default to Runner.TERMINAL_SESSION
executionCommand.runner = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RUNNER,
(intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false) ? Runner.APP_SHELL.getName() : Runner.TERMINAL_SESSION.getName()));
if (Runner.runnerOf(executionCommand.runner) == null) {
errmsg = this.getString(R.string.error_run_command_service_invalid_execution_command_runner, executionCommand.runner);
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return stopService();
}
executionCommand.backgroundCustomLogLevel = IntentUtils.getIntegerExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL, null);
executionCommand.sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION);
executionCommand.shellName = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_SHELL_NAME, null);
executionCommand.shellCreateMode = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_SHELL_CREATE_MODE, null);
executionCommand.commandLabel = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL, "RUN_COMMAND Execution Intent Command");
executionCommand.commandDescription = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION, null);
executionCommand.commandHelp = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_HELP, null);
executionCommand.isPluginExecutionCommand = true;
executionCommand.resultConfig.resultPendingIntent = intent.getParcelableExtra(RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT);
executionCommand.resultConfig.resultDirectoryPath = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_DIRECTORY, null);
if (executionCommand.resultConfig.resultDirectoryPath != null) {
executionCommand.resultConfig.resultSingleFile = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_RESULT_SINGLE_FILE, false);
executionCommand.resultConfig.resultFileBasename = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_BASENAME, null);
executionCommand.resultConfig.resultFileOutputFormat = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, null);
executionCommand.resultConfig.resultFileErrorFormat = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, null);
executionCommand.resultConfig.resultFilesSuffix = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILES_SUFFIX, null);
}
// If "allow-external-apps" property to not set to "true", then just return
// We enable force notifications if "allow-external-apps" policy is violated so that the
// user knows someone tried to run a command in termux context, since it may be malicious
// app or imported (tasker) plugin project and not the user himself. If a pending intent is
// also sent, then its creator is also logged and shown.
errmsg = TermuxPluginUtils.checkIfAllowExternalAppsPolicyIsViolated(this, LOG_TAG);
if (errmsg != null) {
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true);
return stopService();
}
// If executable is null or empty, then exit here instead of getting canonical path which would expand to "/"
if (executionCommand.executable == null || executionCommand.executable.isEmpty()) {
errmsg = this.getString(R.string.error_run_command_service_mandatory_extra_missing, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return stopService();
}
// Get canonical path of executable
executionCommand.executable = TermuxFileUtils.getCanonicalPath(executionCommand.executable, null, true);
// If executable is not a regular file, or is not readable or executable, then just return
// Setting of missing read and execute permissions is not done
error = FileUtils.validateRegularFileExistenceAndPermissions("executable", executionCommand.executable, null,
FileUtils.APP_EXECUTABLE_FILE_PERMISSIONS, true, true,
false);
if (error != null) {
executionCommand.setStateFailed(error);
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return stopService();
}
// If workingDirectory is not null or empty
if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) {
// Get canonical path of workingDirectory
executionCommand.workingDirectory = TermuxFileUtils.getCanonicalPath(executionCommand.workingDirectory, null, true);
// If workingDirectory is not a directory, or is not readable or writable, then just return
// Creation of missing directory and setting of read, write and execute permissions are only done if workingDirectory is
// under allowed termux working directory paths.
// We try to set execute permissions, but ignore if they are missing, since only read and write permissions are required
// for working directories.
error = TermuxFileUtils.validateDirectoryFileExistenceAndPermissions("working", executionCommand.workingDirectory,
true, true, true,
false, true);
if (error != null) {
executionCommand.setStateFailed(error);
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return stopService();
}
}
// If the executable passed as the extra was an applet for coreutils/busybox, then we must
// use it instead of the canonical path above since otherwise arguments would be passed to
// coreutils/busybox instead and command would fail. Broken symlinks would already have been
// validated so it should be fine to use it.
executableExtra = TermuxFileUtils.getExpandedTermuxPath(executableExtra);
if (FileUtils.getFileType(executableExtra, false) == FileType.SYMLINK) {
Logger.logVerbose(LOG_TAG, "The executableExtra path \"" + executableExtra + "\" is a symlink so using it instead of the canonical path \"" + executionCommand.executable + "\"");
executionCommand.executable = executableExtra;
}
executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(executionCommand.executable).build();
Logger.logVerboseExtended(LOG_TAG, executionCommand.toString());
// Create execution intent with the action TERMUX_SERVICE#ACTION_SERVICE_EXECUTE to be sent to the TERMUX_SERVICE
Intent execIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, executionCommand.executableUri);
execIntent.setClass(this, TermuxService.class);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, executionCommand.arguments);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_STDIN, executionCommand.stdin);
if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) execIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, executionCommand.workingDirectory);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RUNNER, executionCommand.runner);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL, DataUtils.getStringFromInteger(executionCommand.backgroundCustomLogLevel, null));
execIntent.putExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION, executionCommand.sessionAction);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_SHELL_NAME, executionCommand.shellName);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_SHELL_CREATE_MODE, executionCommand.shellCreateMode);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_LABEL, executionCommand.commandLabel);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, executionCommand.commandDescription);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_HELP, executionCommand.commandHelp);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP, executionCommand.pluginAPIHelp);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT, executionCommand.resultConfig.resultPendingIntent);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_DIRECTORY, executionCommand.resultConfig.resultDirectoryPath);
if (executionCommand.resultConfig.resultDirectoryPath != null) {
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_SINGLE_FILE, executionCommand.resultConfig.resultSingleFile);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_BASENAME, executionCommand.resultConfig.resultFileBasename);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, executionCommand.resultConfig.resultFileOutputFormat);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, executionCommand.resultConfig.resultFileErrorFormat);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILES_SUFFIX, executionCommand.resultConfig.resultFilesSuffix);
}
// Start TERMUX_SERVICE and pass it execution intent
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
this.startForegroundService(execIntent);
} else {
this.startService(execIntent);
}
return stopService();
}
private int stopService() {
runStopForeground();
return Service.START_NOT_STICKY;
}
private void runStartForeground() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
setupNotificationChannel();
startForeground(TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_ID, buildNotification());
}
}
private void runStopForeground() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
stopForeground(true);
}
}
private Notification buildNotification() {
// Build the notification
Notification.Builder builder = NotificationUtils.geNotificationBuilder(this,
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_LOW,
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_NAME, null, null,
null, null, NotificationUtils.NOTIFICATION_MODE_SILENT);
if (builder == null) return null;
// No need to show a timestamp:
builder.setShowWhen(false);
// Set notification icon
builder.setSmallIcon(R.drawable.ic_service_notification);
// Set background color for small notification icon
builder.setColor(0xFF607D8B);
return builder.build();
}
private void setupNotificationChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
NotificationUtils.setupNotificationChannel(this, TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_ID,
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,135 @@
package com.termux.app;
import android.content.Context;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import android.util.Log;
import androidx.annotation.NonNull;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public final class TermuxAppShell {
public class StreamGobbler extends Thread {
@NonNull
private final String shell;
@NonNull
private final InputStream inputStream;
@NonNull
private final BufferedReader reader;
private static final String LOG_TAG = "termux-tasks";
public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream) {
super("TermuxStreamGobbler");
this.shell = shell;
this.inputStream = inputStream;
reader = new BufferedReader(new InputStreamReader(inputStream));
}
@Override
public void run() {
try {
String line;
while ((line = reader.readLine()) != null) {
// TODO: Is this wait necessary?
// TODO: log
try {
this.wait(128);
} catch (InterruptedException e) {
// no action
}
}
} catch (IOException e) {
// reader probably closed, expected exit condition
}
// make sure our stream is closed and resources will be freed
try {
reader.close();
} catch (IOException e) {
// read already closed
}
}
}
private final Process mProcess;
private final TermuxService mAppShellClient;
private TermuxAppShell(@NonNull final Process process, final TermuxService appShellClient) {
this.mProcess = process;
this.mAppShellClient = appShellClient;
}
public static TermuxAppShell execute(String executable,
String[] arguments,
@NonNull final TermuxService termuxService) {
final String[] commandArray = TermuxShellUtils.setupShellCommandArguments(executable, arguments);
String[] environmentArray = TermuxShellUtils.setupEnvironment(false);
final Process process;
try {
process = Runtime.getRuntime().exec(commandArray, environmentArray, new File(TermuxConstants.HOME_PATH));
} catch (IOException e) {
Log.e(TermuxConstants.LOG_TAG, "Error executing task", e);
return null;
}
final TermuxAppShell appShell = new TermuxAppShell(process, termuxService);
new Thread() {
@Override
public void run() {
try {
appShell.executeInner(termuxService);
} catch (IllegalThreadStateException | InterruptedException e) {
Log.e(TermuxConstants.LOG_TAG, "Error: " + e);
}
}
}.start();
return appShell;
}
private void executeInner(@NonNull final Context context) throws IllegalThreadStateException, InterruptedException {
int mPid = TermuxShellUtils.getPid(mProcess);
DataOutputStream STDIN = new DataOutputStream(mProcess.getOutputStream());
StreamGobbler STDOUT = new StreamGobbler(mPid + "-stdout-gobbler", mProcess.getInputStream());
StreamGobbler STDERR = new StreamGobbler(mPid + "-stderr-gobbler", mProcess.getErrorStream());
STDOUT.start();
STDERR.start();
int exitCode = mProcess.waitFor();
try {
STDIN.close();
} catch (IOException e) {
// might be closed already
}
STDOUT.join();
STDERR.join();
mProcess.destroy();
// TODO: handle exit code, (notify on success, show something more on error)?
}
/**
* Kill this {@link TermuxAppShell} by sending a {@link OsConstants#SIGILL} to its {@link #mProcess}.
*/
public void kill() {
int pid = TermuxShellUtils.getPid(mProcess);
try {
// Send SIGKILL to process
Os.kill(pid, OsConstants.SIGKILL);
} catch (ErrnoException e) {
Log.w(TermuxConstants.LOG_TAG, "Failed to send SIGKILL to AppShell with pid " + pid + ": " + e.getMessage());
}
}
}

View File

@ -1,85 +0,0 @@
package com.termux.app;
import android.app.Application;
import android.content.Context;
import com.termux.BuildConfig;
import com.termux.shared.errors.Error;
import com.termux.shared.logger.Logger;
import com.termux.shared.termux.TermuxBootstrap;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.crash.TermuxCrashUtils;
import com.termux.shared.termux.file.TermuxFileUtils;
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
import com.termux.shared.termux.settings.properties.TermuxAppSharedProperties;
import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment;
import com.termux.shared.termux.shell.am.TermuxAmSocketServer;
import com.termux.shared.termux.shell.TermuxShellManager;
import com.termux.shared.termux.theme.TermuxThemeUtils;
public class TermuxApplication extends Application {
private static final String LOG_TAG = "TermuxApplication";
public void onCreate() {
super.onCreate();
Context context = getApplicationContext();
// Set crash handler for the app
TermuxCrashUtils.setDefaultCrashHandler(this);
// Set log config for the app
setLogConfig(context);
Logger.logDebug("Starting Application");
// Set TermuxBootstrap.TERMUX_APP_PACKAGE_MANAGER and TermuxBootstrap.TERMUX_APP_PACKAGE_VARIANT
TermuxBootstrap.setTermuxPackageManagerAndVariant(BuildConfig.TERMUX_PACKAGE_VARIANT);
// Init app wide SharedProperties loaded from termux.properties
TermuxAppSharedProperties properties = TermuxAppSharedProperties.init(context);
// Init app wide shell manager
TermuxShellManager shellManager = TermuxShellManager.init(context);
// Set NightMode.APP_NIGHT_MODE
TermuxThemeUtils.setAppNightMode(properties.getNightMode());
// Check and create termux files directory. If failed to access it like in case of secondary
// user or external sd card installation, then don't run files directory related code
Error error = TermuxFileUtils.isTermuxFilesDirectoryAccessible(this, true, true);
boolean isTermuxFilesDirectoryAccessible = error == null;
if (isTermuxFilesDirectoryAccessible) {
Logger.logInfo(LOG_TAG, "Termux files directory is accessible");
error = TermuxFileUtils.isAppsTermuxAppDirectoryAccessible(true, true);
if (error != null) {
Logger.logErrorExtended(LOG_TAG, "Create apps/termux-app directory failed\n" + error);
return;
}
// Setup termux-am-socket server
TermuxAmSocketServer.setupTermuxAmSocketServer(context);
} else {
Logger.logErrorExtended(LOG_TAG, "Termux files directory is not accessible\n" + error);
}
// Init TermuxShellEnvironment constants and caches after everything has been setup including termux-am-socket server
TermuxShellEnvironment.init(this);
if (isTermuxFilesDirectoryAccessible) {
TermuxShellEnvironment.writeEnvironmentToFile(this);
}
}
public static void setLogConfig(Context context) {
Logger.setDefaultLogTag(TermuxConstants.TERMUX_APP_NAME);
// Load the log level from shared preferences and set it to the {@link Logger.CURRENT_LOG_LEVEL}
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
if (preferences == null) return;
preferences.setLogLevel(null, preferences.getLogLevel());
}
}

View File

@ -0,0 +1,14 @@
package com.termux.app;
public class TermuxConstants {
public static final String LOG_TAG = "termux";
public static final String FILES_PATH = "/data/data/com.termux/files";
public static final String PREFIX_PATH = FILES_PATH + "/usr";
public static final String BIN_PATH = PREFIX_PATH + "/bin";
public static final String HOME_PATH = FILES_PATH + "/home";
public static final int TERMUX_APP_NOTIFICATION_ID = 1337;
}

View File

@ -1,4 +1,4 @@
package com.termux.filepicker;
package com.termux.app;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
@ -12,7 +12,6 @@ import android.provider.DocumentsProvider;
import android.webkit.MimeTypeMap;
import com.termux.R;
import com.termux.shared.termux.TermuxConstants;
import java.io.File;
import java.io.FileNotFoundException;
@ -35,7 +34,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
private static final String ALL_MIME_TYPES = "*/*";
private static final File BASE_DIR = TermuxConstants.TERMUX_HOME_DIR;
private static final File BASE_DIR = new File(TermuxConstants.HOME_PATH);
// The default columns to return information about a root if no specific
@ -171,7 +170,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
// through the whole SD card).
boolean isInsideHome;
try {
isInsideHome = file.getCanonicalPath().startsWith(TermuxConstants.TERMUX_HOME_DIR_PATH);
isInsideHome = file.getCanonicalPath().startsWith(TermuxConstants.HOME_PATH);
} catch (IOException e) {
isInsideHome = true;
}

View File

@ -1,30 +1,16 @@
package com.termux.app.api.file;
package com.termux.app;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.provider.OpenableColumns;
import android.util.Log;
import android.util.Patterns;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import com.termux.R;
import com.termux.shared.android.PackageUtils;
import com.termux.shared.data.DataUtils;
import com.termux.shared.data.IntentUtils;
import com.termux.shared.net.uri.UriUtils;
import com.termux.shared.interact.MessageDialogUtils;
import com.termux.shared.net.uri.UriScheme;
import com.termux.shared.termux.interact.TextInputDialogUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
import com.termux.app.TermuxService;
import com.termux.shared.logger.Logger;
import com.termux.shared.termux.settings.properties.TermuxAppSharedProperties;
import com.termux.shared.termux.settings.properties.TermuxPropertyConstants;
import java.io.ByteArrayInputStream;
import java.io.File;
@ -36,11 +22,11 @@ import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.regex.Pattern;
public class FileReceiverActivity extends AppCompatActivity {
public class TermuxFileReceiverActivity extends AppCompatActivity {
static final String TERMUX_RECEIVEDIR = TermuxConstants.TERMUX_FILES_DIR_PATH + "/home/downloads";
static final String EDITOR_PROGRAM = TermuxConstants.TERMUX_HOME_DIR_PATH + "/bin/termux-file-editor";
static final String URL_OPENER_PROGRAM = TermuxConstants.TERMUX_HOME_DIR_PATH + "/bin/termux-url-opener";
static final String TERMUX_RECEIVEDIR = TermuxConstants.HOME_PATH + "/downloads";
static final String EDITOR_PROGRAM = TermuxConstants.HOME_PATH + "/bin/termux-file-editor";
static final String URL_OPENER_PROGRAM = TermuxConstants.HOME_PATH + "/bin/termux-url-opener";
/**
* If the activity should be finished when the name input dialog is dismissed. This is disabled
@ -50,8 +36,6 @@ public class FileReceiverActivity extends AppCompatActivity {
*/
boolean mFinishOnDismissNameDialog = true;
private static final String API_TAG = TermuxConstants.TERMUX_APP_NAME + "FileReceiver";
private static final String LOG_TAG = "FileReceiverActivity";
static boolean isSharedTextAnUrl(String sharedText) {
@ -68,9 +52,7 @@ public class FileReceiverActivity extends AppCompatActivity {
final String type = intent.getType();
final String scheme = intent.getScheme();
Logger.logVerbose(LOG_TAG, "Intent Received:\n" + IntentUtils.getIntentString(intent));
final String sharedTitle = IntentUtils.getStringExtraIfSet(intent, Intent.EXTRA_TITLE, null);
final String sharedTitle = intent.getStringExtra(Intent.EXTRA_TITLE);
if (Intent.ACTION_SEND.equals(action) && type != null) {
final String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
@ -82,7 +64,7 @@ public class FileReceiverActivity extends AppCompatActivity {
if (isSharedTextAnUrl(sharedText)) {
handleUrlAndFinish(sharedText);
} else {
String subject = IntentUtils.getStringExtraIfSet(intent, Intent.EXTRA_SUBJECT, null);
String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
if (subject == null) subject = sharedTitle;
if (subject != null) subject += ".txt";
promptNameAndSave(new ByteArrayInputStream(sharedText.getBytes(StandardCharsets.UTF_8)), subject);
@ -98,14 +80,13 @@ public class FileReceiverActivity extends AppCompatActivity {
return;
}
if (UriScheme.SCHEME_CONTENT.equals(scheme)) {
if ("content".equals(scheme)) {
handleContentUri(dataUri, sharedTitle);
} else if (UriScheme.SCHEME_FILE.equals(scheme)) {
Logger.logVerbose(LOG_TAG, "uri: \"" + dataUri + "\", path: \"" + dataUri.getPath() + "\", fragment: \"" + dataUri.getFragment() + "\"");
} else if ("file".equals(scheme)) {
Log.v(LOG_TAG, "uri: \"" + dataUri + "\", path: \"" + dataUri.getPath() + "\", fragment: \"" + dataUri.getFragment() + "\"");
// Get full path including fragment (anything after last "#")
String path = UriUtils.getUriFilePathWithFragment(dataUri);
if (DataUtils.isNullOrEmpty(path)) {
String path = dataUri.getPath();
if (path == null || path.isEmpty()) {
showErrorDialogAndQuit("File path from data uri is null, empty or invalid.");
return;
}
@ -125,19 +106,13 @@ public class FileReceiverActivity extends AppCompatActivity {
void showErrorDialogAndQuit(String message) {
mFinishOnDismissNameDialog = false;
MessageDialogUtils.showMessage(this,
API_TAG, message,
null, (dialog, which) -> finish(),
null, null,
dialog -> finish());
TermuxMessageDialogUtils.showMessage(this, "Termux", message, null, (dialog, which) -> finish(), null, null, dialog -> finish());
}
void handleContentUri(@NonNull final Uri uri, String subjectFromIntent) {
try {
Logger.logVerbose(LOG_TAG, "uri: \"" + uri + "\", path: \"" + uri.getPath() + "\", fragment: \"" + uri.getFragment() + "\"");
Log.v(LOG_TAG, "uri: \"" + uri + "\", path: \"" + uri.getPath() + "\", fragment: \"" + uri.getFragment() + "\"");
String attachmentFileName = null;
String[] projection = new String[]{OpenableColumns.DISPLAY_NAME};
try (Cursor c = getContentResolver().query(uri, projection, null, null, null)) {
if (c != null && c.moveToFirst()) {
@ -146,19 +121,23 @@ public class FileReceiverActivity extends AppCompatActivity {
}
}
if (attachmentFileName == null) attachmentFileName = subjectFromIntent;
if (attachmentFileName == null) attachmentFileName = UriUtils.getUriFileBasename(uri, true);
if (attachmentFileName == null) {
attachmentFileName = subjectFromIntent;
}
if (attachmentFileName == null) {
attachmentFileName = new File(uri.getPath()).getName();
}
InputStream in = getContentResolver().openInputStream(uri);
promptNameAndSave(in, attachmentFileName);
} catch (Exception e) {
showErrorDialogAndQuit("Unable to handle shared content:\n\n" + e.getMessage());
Logger.logStackTraceWithMessage(LOG_TAG, "handleContentUri(uri=" + uri + ") failed", e);
Log.e(LOG_TAG, "handleContentUri(uri=" + uri + ") failed", e);
}
}
void promptNameAndSave(final InputStream in, final String attachmentFileName) {
TextInputDialogUtils.textInput(this, R.string.title_file_received, attachmentFileName,
TermuxMessageDialogUtils.textInput(this, R.string.title_file_received, attachmentFileName,
R.string.action_file_received_edit, text -> {
File outFile = saveStreamWithName(in, text);
if (outFile == null) return;
@ -174,20 +153,20 @@ public class FileReceiverActivity extends AppCompatActivity {
//noinspection ResultOfMethodCallIgnored
editorProgramFile.setExecutable(true);
final Uri scriptUri = UriUtils.getFileUri(EDITOR_PROGRAM);
final Uri scriptUri = new Uri.Builder().scheme("file").path(EDITOR_PROGRAM).build();
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, scriptUri);
executeIntent.setClass(FileReceiverActivity.this, TermuxService.class);
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()});
Intent executeIntent = new Intent(TermuxService.ACTION_SERVICE_EXECUTE, scriptUri);
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
executeIntent.putExtra(TermuxService.TERMUX_EXECUTE_EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()});
startService(executeIntent);
finish();
},
R.string.action_file_received_open_directory, text -> {
if (saveStreamWithName(in, text) == null) return;
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE);
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, TERMUX_RECEIVEDIR);
executeIntent.setClass(FileReceiverActivity.this, TermuxService.class);
Intent executeIntent = new Intent(TermuxService.ACTION_SERVICE_EXECUTE);
executeIntent.putExtra(TermuxService.TERMUX_EXECUTE_WORKDIR, TERMUX_RECEIVEDIR);
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
startService(executeIntent);
finish();
},
@ -199,7 +178,7 @@ public class FileReceiverActivity extends AppCompatActivity {
public File saveStreamWithName(InputStream in, String attachmentFileName) {
File receiveDir = new File(TERMUX_RECEIVEDIR);
if (DataUtils.isNullOrEmpty(attachmentFileName)) {
if (attachmentFileName == null || attachmentFileName.isEmpty()) {
showErrorDialogAndQuit("File name cannot be null or empty");
return null;
}
@ -221,7 +200,7 @@ public class FileReceiverActivity extends AppCompatActivity {
return outFile;
} catch (IOException e) {
showErrorDialogAndQuit("Error saving file:\n\n" + e);
Logger.logStackTraceWithMessage(LOG_TAG, "Error saving file", e);
Log.e(LOG_TAG, "Error saving file", e);
return null;
}
}
@ -238,48 +217,13 @@ public class FileReceiverActivity extends AppCompatActivity {
//noinspection ResultOfMethodCallIgnored
urlOpenerProgramFile.setExecutable(true);
final Uri urlOpenerProgramUri = UriUtils.getFileUri(URL_OPENER_PROGRAM);
final Uri urlOpenerProgramUri = new Uri.Builder().scheme("file").path(URL_OPENER_PROGRAM).build();
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, urlOpenerProgramUri);
executeIntent.setClass(FileReceiverActivity.this, TermuxService.class);
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, new String[]{url});
Intent executeIntent = new Intent(TermuxService.ACTION_SERVICE_EXECUTE, urlOpenerProgramUri);
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
executeIntent.putExtra(TermuxService.TERMUX_EXECUTE_EXTRA_ARGUMENTS, new String[]{url});
startService(executeIntent);
finish();
}
/**
* Update {@link TERMUX_APP#FILE_SHARE_RECEIVER_ACTIVITY_CLASS_NAME} component state depending on
* {@link TermuxPropertyConstants#KEY_DISABLE_FILE_SHARE_RECEIVER} value and
* {@link TERMUX_APP#FILE_VIEW_RECEIVER_ACTIVITY_CLASS_NAME} component state depending on
* {@link TermuxPropertyConstants#KEY_DISABLE_FILE_VIEW_RECEIVER} value.
*/
public static void updateFileReceiverActivityComponentsState(@NonNull Context context) {
new Thread() {
@Override
public void run() {
TermuxAppSharedProperties properties = TermuxAppSharedProperties.getProperties();
String errmsg;
boolean state;
state = !properties.isFileShareReceiverDisabled();
Logger.logVerbose(LOG_TAG, "Setting " + TERMUX_APP.FILE_SHARE_RECEIVER_ACTIVITY_CLASS_NAME + " component state to " + state);
errmsg = PackageUtils.setComponentState(context,TermuxConstants.TERMUX_PACKAGE_NAME,
TERMUX_APP.FILE_SHARE_RECEIVER_ACTIVITY_CLASS_NAME,
state, null, false, false);
if (errmsg != null)
Logger.logError(LOG_TAG, errmsg);
state = !properties.isFileViewReceiverDisabled();
Logger.logVerbose(LOG_TAG, "Setting " + TERMUX_APP.FILE_VIEW_RECEIVER_ACTIVITY_CLASS_NAME + " component state to " + state);
errmsg = PackageUtils.setComponentState(context,TermuxConstants.TERMUX_PACKAGE_NAME,
TERMUX_APP.FILE_VIEW_RECEIVER_ACTIVITY_CLASS_NAME,
state, null, false, false);
if (errmsg != null)
Logger.logError(LOG_TAG, errmsg);
}
}.start();
}
}

View File

@ -1,4 +1,4 @@
package com.termux.app.activities;
package com.termux.app;
import android.content.ActivityNotFoundException;
import android.content.Intent;
@ -13,10 +13,10 @@ import android.widget.RelativeLayout;
import androidx.appcompat.app.AppCompatActivity;
import com.termux.shared.termux.TermuxConstants;
/** Basic embedded browser for viewing help pages. */
public final class HelpActivity extends AppCompatActivity {
public final class TermuxHelpActivity extends AppCompatActivity {
public static final String TERMUX_WIKI_URL = "https://wiki.termux.com"; // Default: "https://wiki.termux.com"
WebView mWebView;
@ -35,14 +35,13 @@ public final class HelpActivity extends AppCompatActivity {
mWebView = new WebView(this);
WebSettings settings = mWebView.getSettings();
settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
settings.setAppCacheEnabled(false);
setContentView(progressLayout);
mWebView.clearCache(true);
mWebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.equals(TermuxConstants.TERMUX_WIKI_URL) || url.startsWith(TermuxConstants.TERMUX_WIKI_URL + "/")) {
if (url.equals(TERMUX_WIKI_URL) || url.startsWith(TERMUX_WIKI_URL + "/")) {
// Inline help.
setContentView(progressLayout);
return false;
@ -63,7 +62,7 @@ public final class HelpActivity extends AppCompatActivity {
setContentView(mWebView);
}
});
mWebView.loadUrl(TermuxConstants.TERMUX_WIKI_URL);
mWebView.loadUrl(TERMUX_WIKI_URL);
}
@Override

View File

@ -4,24 +4,17 @@ import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.Build;
import android.content.pm.ApplicationInfo;
import android.os.Environment;
import android.os.UserHandle;
import android.os.UserManager;
import android.system.ErrnoException;
import android.system.Os;
import android.util.Log;
import android.util.Pair;
import android.view.WindowManager;
import com.termux.R;
import com.termux.shared.file.FileUtils;
import com.termux.shared.termux.crash.TermuxCrashUtils;
import com.termux.shared.termux.file.TermuxFileUtils;
import com.termux.shared.interact.MessageDialogUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.errors.Error;
import com.termux.shared.android.PackageUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxUtils;
import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
@ -33,11 +26,6 @@ import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import static com.termux.shared.termux.TermuxConstants.TERMUX_PREFIX_DIR;
import static com.termux.shared.termux.TermuxConstants.TERMUX_PREFIX_DIR_PATH;
import static com.termux.shared.termux.TermuxConstants.TERMUX_STAGING_PREFIX_DIR;
import static com.termux.shared.termux.TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH;
/**
* Install the Termux bootstrap packages if necessary by following the below steps:
* <p/>
@ -59,59 +47,42 @@ import static com.termux.shared.termux.TermuxConstants.TERMUX_STAGING_PREFIX_DIR
*/
final class TermuxInstaller {
private static final String TERMUX_STAGING_PREFIX_DIR_PATH = TermuxConstants.FILES_PATH + "/usr-staging"; // Default: "/data/data/com.termux/files/usr-staging"
private static final String LOG_TAG = "TermuxInstaller";
/** Performs bootstrap setup if necessary. */
/**
* Performs bootstrap setup if necessary.
*/
static void setupBootstrapIfNeeded(final Activity activity, final Runnable whenDone) {
String bootstrapErrorMessage;
Error filesDirectoryAccessibleError;
// This will also call Context.getFilesDir(), which should ensure that termux files directory
// is created if it does not already exist
filesDirectoryAccessibleError = TermuxFileUtils.isTermuxFilesDirectoryAccessible(activity, true, true);
boolean isFilesDirectoryAccessible = filesDirectoryAccessibleError == null;
// Ensure that termux files directory is created if it does not already exist:
new File(activity.getFilesDir(), "home").mkdir();
// Termux can only be run as the primary user (device owner) since only that
// account has the expected file system paths. Verify that:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && !PackageUtils.isCurrentUserThePrimaryUser(activity)) {
bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message,
MarkdownUtils.getMarkdownCodeForString(TERMUX_PREFIX_DIR_PATH, false));
Logger.logError(LOG_TAG, "isFilesDirectoryAccessible: " + isFilesDirectoryAccessible);
Logger.logError(LOG_TAG, bootstrapErrorMessage);
sendBootstrapCrashReportNotification(activity, bootstrapErrorMessage);
MessageDialogUtils.exitAppWithErrorMessage(activity,
activity.getString(R.string.bootstrap_error_title),
bootstrapErrorMessage);
UserManager userManager = (UserManager) activity.getSystemService(Context.USER_SERVICE);
boolean isCurrentUserPrimary = userManager.getSerialNumberForUser(UserHandle.getUserHandleForUid(activity.getApplicationInfo().uid)) == 0;
if (!isCurrentUserPrimary) {
bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message);
TermuxMessageDialogUtils.exitAppWithErrorMessage(activity, activity.getString(R.string.bootstrap_error_title), bootstrapErrorMessage);
return;
}
if (!isFilesDirectoryAccessible) {
bootstrapErrorMessage = Error.getMinimalErrorString(filesDirectoryAccessibleError);
//noinspection SdCardPath
if (PackageUtils.isAppInstalledOnExternalStorage(activity) &&
!TermuxConstants.TERMUX_FILES_DIR_PATH.equals(activity.getFilesDir().getAbsolutePath().replaceAll("^/data/user/0/", "/data/data/"))) {
bootstrapErrorMessage += "\n\n" + activity.getString(R.string.bootstrap_error_installed_on_portable_sd,
MarkdownUtils.getMarkdownCodeForString(TERMUX_PREFIX_DIR_PATH, false));
}
Logger.logError(LOG_TAG, bootstrapErrorMessage);
sendBootstrapCrashReportNotification(activity, bootstrapErrorMessage);
MessageDialogUtils.showMessage(activity,
activity.getString(R.string.bootstrap_error_title),
bootstrapErrorMessage, null);
boolean isInstalledOnExternalStorage = (activity.getApplicationInfo().flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0;
if (isInstalledOnExternalStorage) {
new AlertDialog.Builder(activity)
.setTitle(R.string.bootstrap_error_installed_on_portable_sd)
.show();
return;
}
// If prefix directory exists, even if its a symlink to a valid directory and symlink is not broken/dangling
if (FileUtils.directoryFileExists(TERMUX_PREFIX_DIR_PATH, true)) {
if (TermuxFileUtils.isTermuxPrefixDirectoryEmpty()) {
Logger.logInfo(LOG_TAG, "The termux prefix directory \"" + TERMUX_PREFIX_DIR_PATH + "\" exists but is empty or only contains specific unimportant files.");
} else {
whenDone.run();
return;
}
} else if (FileUtils.fileExists(TERMUX_PREFIX_DIR_PATH, false)) {
Logger.logInfo(LOG_TAG, "The termux prefix directory \"" + TERMUX_PREFIX_DIR_PATH + "\" does not exist but another file exists at its destination.");
if (new File(TermuxConstants.PREFIX_PATH).exists()) {
whenDone.run();
return;
}
final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false);
@ -119,40 +90,19 @@ final class TermuxInstaller {
@Override
public void run() {
try {
Logger.logInfo(LOG_TAG, "Installing " + TermuxConstants.TERMUX_APP_NAME + " bootstrap packages.");
Error error;
// Delete prefix staging directory or any file at its destination
error = FileUtils.deleteFile("termux prefix staging directory", TERMUX_STAGING_PREFIX_DIR_PATH, true);
if (error != null) {
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
File stagingPrefixFile = new File(TERMUX_STAGING_PREFIX_DIR_PATH);
if (stagingPrefixFile.exists() && !deleteDir(stagingPrefixFile)) {
showBootstrapErrorDialog(activity, whenDone, "Unable to delete old staging area.");
return;
}
// Delete prefix directory or any file at its destination
error = FileUtils.deleteFile("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true);
if (error != null) {
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
File prefixFile = new File(TERMUX_STAGING_PREFIX_DIR_PATH);
if (prefixFile.exists() && !deleteDir(prefixFile)) {
showBootstrapErrorDialog(activity, whenDone, "Unable to delete old PREFIX.");
return;
}
// Create prefix staging directory if it does not already exist and set required permissions
error = TermuxFileUtils.isTermuxPrefixStagingDirectoryAccessible(true, true);
if (error != null) {
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
return;
}
// Create prefix directory if it does not already exist and set required permissions
error = TermuxFileUtils.isTermuxPrefixDirectoryAccessible(true, true);
if (error != null) {
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
return;
}
Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + TERMUX_STAGING_PREFIX_DIR_PATH + "\".");
final byte[] buffer = new byte[8096];
final List<Pair<String, String>> symlinks = new ArrayList<>(50);
@ -170,25 +120,15 @@ final class TermuxInstaller {
String oldPath = parts[0];
String newPath = TERMUX_STAGING_PREFIX_DIR_PATH + "/" + parts[1];
symlinks.add(Pair.create(oldPath, newPath));
error = ensureDirectoryExists(new File(newPath).getParentFile());
if (error != null) {
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
return;
}
}
} else {
String zipEntryName = zipEntry.getName();
File targetFile = new File(TERMUX_STAGING_PREFIX_DIR_PATH, zipEntryName);
boolean isDirectory = zipEntry.isDirectory();
error = ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile());
if (error != null) {
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
return;
}
if (!isDirectory) {
if (isDirectory) {
targetFile.mkdirs();
} else {
try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
int readBytes;
while ((readBytes = zipInput.read(buffer)) != -1)
@ -210,22 +150,12 @@ final class TermuxInstaller {
Os.symlink(symlink.first, symlink.second);
}
Logger.logInfo(LOG_TAG, "Moving termux prefix staging to prefix directory.");
if (!TERMUX_STAGING_PREFIX_DIR.renameTo(TERMUX_PREFIX_DIR)) {
throw new RuntimeException("Moving termux prefix staging to prefix directory failed");
}
Logger.logInfo(LOG_TAG, "Bootstrap packages installed successfully.");
// Recreate env file since termux prefix was wiped earlier
TermuxShellEnvironment.writeEnvironmentToFile(activity);
Os.rename(TERMUX_STAGING_PREFIX_DIR_PATH, TermuxConstants.PREFIX_PATH);
activity.runOnUiThread(whenDone);
} catch (final Exception e) {
showBootstrapErrorDialog(activity, whenDone, Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)));
Log.e(LOG_TAG, "Error in installation", e);
showBootstrapErrorDialog(activity, whenDone, "Error in installation: " + e.getMessage());
} finally {
activity.runOnUiThread(() -> {
try {
@ -240,10 +170,7 @@ final class TermuxInstaller {
}
public static void showBootstrapErrorDialog(Activity activity, Runnable whenDone, String message) {
Logger.logErrorExtended(LOG_TAG, "Bootstrap Error:\n" + message);
// Send a notification with the exception so that the user knows why bootstrap setup failed
sendBootstrapCrashReportNotification(activity, message);
Log.e(LOG_TAG, "Bootstrap Error: " + message);
activity.runOnUiThread(() -> {
try {
@ -254,7 +181,7 @@ final class TermuxInstaller {
})
.setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
dialog.dismiss();
FileUtils.deleteFile("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true);
deleteDir(new File(TermuxConstants.PREFIX_PATH));
TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone);
}).show();
} catch (WindowManager.BadTokenException e1) {
@ -263,40 +190,19 @@ final class TermuxInstaller {
});
}
private static void sendBootstrapCrashReportNotification(Activity activity, String message) {
final String title = TermuxConstants.TERMUX_APP_NAME + " Bootstrap Error";
// Add info of all install Termux plugin apps as well since their target sdk or installation
// on external/portable sd card can affect Termux app files directory access or exec.
TermuxCrashUtils.sendCrashReportNotification(activity, LOG_TAG,
title, null, "## " + title + "\n\n" + message + "\n\n" +
TermuxUtils.getTermuxDebugMarkdownString(activity),
true, false, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGES, true);
}
static void setupStorageSymlinks(final Context context) {
final String LOG_TAG = "termux-storage";
final String title = TermuxConstants.TERMUX_APP_NAME + " Setup Storage Error";
Logger.logInfo(LOG_TAG, "Setting up storage symlinks.");
Log.i(LOG_TAG, "Setting up storage symlinks.");
new Thread() {
public void run() {
try {
Error error;
File storageDir = TermuxConstants.TERMUX_STORAGE_HOME_DIR;
File storageDir = new File(TermuxConstants.HOME_PATH + "/storage");
error = FileUtils.clearDirectory("~/storage", storageDir.getAbsolutePath());
if (error != null) {
Logger.logErrorAndShowToast(context, LOG_TAG, error.getMessage());
Logger.logErrorExtended(LOG_TAG, "Setup Storage Error\n" + error.toString());
TermuxCrashUtils.sendCrashReportNotification(context, LOG_TAG, title, null,
"## " + title + "\n\n" + Error.getErrorMarkdownString(error),
true, false, TermuxUtils.AppInfoMode.TERMUX_PACKAGE, true);
return;
if (!clearDirectory(storageDir)) {
throw new RuntimeException("Unable to clear ~/storage");
}
Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/shared, ~/storage/downloads, ~/storage/dcim, ~/storage/pictures, ~/storage/music and ~/storage/movies for directories in \"" + Environment.getExternalStorageDirectory().getAbsolutePath() + "\".");
Log.i(LOG_TAG, "Setting up storage symlinks at ~/storage/shared, ~/storage/downloads, ~/storage/dcim, ~/storage/pictures, ~/storage/music and ~/storage/movies for directories in \"" + Environment.getExternalStorageDirectory().getAbsolutePath() + "\".");
// Get primary storage root "/storage/emulated/0" symlink
File sharedDir = Environment.getExternalStorageDirectory();
@ -342,7 +248,7 @@ final class TermuxInstaller {
File dir = dirs[i];
if (dir == null) continue;
String symlinkName = "external-" + i;
Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/" + symlinkName + " for \"" + dir.getAbsolutePath() + "\".");
Log.i(LOG_TAG, "Setting up storage symlinks at ~/storage/" + symlinkName + " for \"" + dir.getAbsolutePath() + "\".");
Os.symlink(dir.getAbsolutePath(), new File(storageDir, symlinkName).getAbsolutePath());
}
}
@ -354,25 +260,28 @@ final class TermuxInstaller {
File dir = dirs[i];
if (dir == null) continue;
String symlinkName = "media-" + i;
Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/" + symlinkName + " for \"" + dir.getAbsolutePath() + "\".");
Log.i(LOG_TAG, "Setting up storage symlinks at ~/storage/" + symlinkName + " for \"" + dir.getAbsolutePath() + "\".");
Os.symlink(dir.getAbsolutePath(), new File(storageDir, symlinkName).getAbsolutePath());
}
}
Logger.logInfo(LOG_TAG, "Storage symlinks created successfully.");
} catch (Exception e) {
Logger.logErrorAndShowToast(context, LOG_TAG, e.getMessage());
Logger.logStackTraceWithMessage(LOG_TAG, "Setup Storage Error: Error setting up link", e);
TermuxCrashUtils.sendCrashReportNotification(context, LOG_TAG, title, null,
"## " + title + "\n\n" + Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)),
true, false, TermuxUtils.AppInfoMode.TERMUX_PACKAGE, true);
} catch (ErrnoException e) {
throw new RuntimeException(e);
}
}
}.start();
}
private static Error ensureDirectoryExists(File directory) {
return FileUtils.createDirectoryFile(directory.getAbsolutePath());
public static boolean deleteDir(File dir) {
if (dir.isDirectory()) {
String[] children = dir.list();
for (int i = 0; i < children.length; i++) {
boolean success = deleteDir(new File(dir, children[i]));
if (!success) {
return false;
}
}
}
return dir.delete();
}
public static byte[] loadZipBytes() {
@ -383,4 +292,12 @@ final class TermuxInstaller {
public static native byte[] getZip();
private static boolean clearDirectory(File directory) {
for (File child : directory.listFiles()) {
if (!clearDirectory(child)) {
return false;
}
}
return directory.delete();
}
}

View File

@ -1,16 +1,58 @@
package com.termux.shared.termux.interact;
package com.termux.app;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Handler;
import android.os.Looper;
import android.text.Selection;
import android.util.TypedValue;
import android.view.KeyEvent;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.Toast;
public final class TextInputDialogUtils {
public class TermuxMessageDialogUtils {
public static void showMessage(Context context, String titleText, String messageText, final DialogInterface.OnDismissListener onDismiss) {
showMessage(context, titleText, messageText, null, null, null, null, onDismiss);
}
public static void showMessage(Context context,
String titleText,
String messageText,
String positiveText,
final DialogInterface.OnClickListener onPositiveButton,
String negativeText,
final DialogInterface.OnClickListener onNegativeButton,
final DialogInterface.OnDismissListener onDismiss
) {
AlertDialog.Builder builder = new AlertDialog.Builder(context)
.setTitle(titleText)
.setMessage(messageText)
.setPositiveButton(positiveText == null ? context.getString(android.R.string.ok) : positiveText, onPositiveButton);
if (negativeText != null) {
builder.setNegativeButton(negativeText, onNegativeButton);
}
if (onDismiss != null) {
builder.setOnDismissListener(onDismiss);
}
builder.show();
}
public static void exitAppWithErrorMessage(Context context, String titleText, String messageText) {
showMessage(context, titleText, messageText, dialog -> System.exit(0));
}
public static void showToast(Context context, String message) {
new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(context, message, Toast.LENGTH_LONG).show());
}
public interface TextSetListener {
void onTextSet(String text);
@ -43,7 +85,7 @@ public final class TextInputDialogUtils {
LinearLayout layout = new LinearLayout(activity);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
layout.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
layout.setPadding(paddingTopAndSides, paddingTopAndSides, paddingTopAndSides, paddingBottom);
layout.addView(input);
@ -69,4 +111,6 @@ public final class TextInputDialogUtils {
dialogHolder[0].show();
}
}

View File

@ -11,16 +11,9 @@ import android.net.Uri;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
import android.util.Log;
import android.webkit.MimeTypeMap;
import com.termux.shared.termux.plugins.TermuxPluginUtils;
import com.termux.shared.data.DataUtils;
import com.termux.shared.data.IntentUtils;
import com.termux.shared.net.uri.UriUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.net.uri.UriScheme;
import com.termux.shared.termux.TermuxConstants;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
@ -35,12 +28,11 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
final Uri data = intent.getData();
if (data == null) {
Logger.logError(LOG_TAG, "Called without intent data");
Log.e(LOG_TAG, "Called without intent data");
return;
}
Logger.logVerbose(LOG_TAG, "Intent Received:\n" + IntentUtils.getIntentString(intent));
Logger.logVerbose(LOG_TAG, "uri: \"" + data + "\", path: \"" + data.getPath() + "\", fragment: \"" + data.getFragment() + "\"");
Log.v(LOG_TAG, "uri: \"" + data + "\", path: \"" + data.getPath() + "\", fragment: \"" + data.getFragment() + "\"");
final String contentTypeExtra = intent.getStringExtra("content-type");
final boolean useChooser = intent.getBooleanExtra("chooser", false);
@ -51,12 +43,12 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
// Ok.
break;
default:
Logger.logError(LOG_TAG, "Invalid action '" + intentAction + "', using 'view'");
Log.e(LOG_TAG, "Invalid action '" + intentAction + "', using 'view'");
break;
}
String scheme = data.getScheme();
if (scheme != null && !UriScheme.SCHEME_FILE.equals(scheme)) {
if (scheme != null && !"file".equals(scheme)) {
Intent urlIntent = new Intent(intentAction, data);
if (intentAction.equals(Intent.ACTION_SEND)) {
urlIntent.putExtra(Intent.EXTRA_TEXT, data.toString());
@ -68,21 +60,21 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
try {
context.startActivity(urlIntent);
} catch (ActivityNotFoundException e) {
Logger.logError(LOG_TAG, "No app handles the url " + data);
Log.e(LOG_TAG, "No app handles the url " + data);
}
return;
}
// Get full path including fragment (anything after last "#")
String filePath = UriUtils.getUriFilePathWithFragment(data);
if (DataUtils.isNullOrEmpty(filePath)) {
Logger.logError(LOG_TAG, "filePath is null or empty");
String filePath = data.getPath();
if (filePath == null || filePath.isEmpty()) {
Log.e(LOG_TAG, "filePath is null or empty");
return;
}
final File fileToShare = new File(filePath);
if (!(fileToShare.isFile() && fileToShare.canRead())) {
Logger.logError(LOG_TAG, "Not a readable file: '" + fileToShare.getAbsolutePath() + "'");
Log.e(LOG_TAG, "Not a readable file: '" + fileToShare.getAbsolutePath() + "'");
return;
}
@ -104,7 +96,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
}
// Do not create Uri with Uri.parse() and use Uri.Builder().path(), check UriUtils.getUriFilePath().
Uri uriToShare = UriUtils.getContentUri(TermuxConstants.TERMUX_FILE_SHARE_URI_AUTHORITY, fileToShare.getAbsolutePath());
Uri uriToShare = new Uri.Builder().scheme("content").authority("com.termux.files").path(fileToShare.getAbsolutePath()).build();
if (Intent.ACTION_SEND.equals(intentAction)) {
sendIntent.putExtra(Intent.EXTRA_STREAM, uriToShare);
@ -120,7 +112,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
try {
context.startActivity(sendIntent);
} catch (ActivityNotFoundException e) {
Logger.logError(LOG_TAG, "No app handles the url " + data);
Log.e(LOG_TAG, "No app handles the url " + data);
}
}
@ -195,28 +187,12 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
File file = new File(uri.getPath());
try {
String path = file.getCanonicalPath();
String callingPackageName = getCallingPackage();
Logger.logDebug(LOG_TAG, "Open file request received from " + callingPackageName + " for \"" + path + "\" with mode \"" + mode + "\"");
// String callingPackageName = getCallingPackage();
String storagePath = Environment.getExternalStorageDirectory().getCanonicalPath();
// See https://support.google.com/faqs/answer/7496913:
if (!(path.startsWith(TermuxConstants.TERMUX_FILES_DIR_PATH) || path.startsWith(storagePath))) {
if (!(path.startsWith(TermuxConstants.FILES_PATH) || path.startsWith(storagePath))) {
throw new IllegalArgumentException("Invalid path: " + path);
}
// If TermuxConstants.PROP_ALLOW_EXTERNAL_APPS property to not set to "true", then throw exception
String errmsg = TermuxPluginUtils.checkIfAllowExternalAppsPolicyIsViolated(getContext(), LOG_TAG);
if (errmsg != null) {
throw new IllegalArgumentException(errmsg);
}
// **DO NOT** allow these files to be modified by ContentProvider exposed to external
// apps, since they may silently modify the values for security properties like
// TermuxConstants.PROP_ALLOW_EXTERNAL_APPS set by users without their explicit consent.
if (TermuxConstants.TERMUX_PROPERTIES_FILE_PATHS_LIST.contains(path) ||
TermuxConstants.TERMUX_FLOAT_PROPERTIES_FILE_PATHS_LIST.contains(path)) {
mode = "r";
}
} catch (IOException e) {
throw new IllegalArgumentException(e);
}

View File

@ -0,0 +1,171 @@
package com.termux.app;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.PowerManager;
import android.provider.Settings;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class TermuxPermissionUtils {
public static final int REQUEST_GRANT_STORAGE_PERMISSION = 1000;
public static final int REQUEST_DISABLE_BATTERY_OPTIMIZATIONS = 2000;
public static final int REQUEST_GRANT_DISPLAY_OVER_OTHER_APPS_PERMISSION = 2001;
private static final String LOG_TAG = "PermissionUtils";
/**
* Check if app has been granted the required permission.
*
* @param context The context for operations.
* @param permission The {@link String} name for permission to check.
* @return Returns {@code true} if permission is granted, otherwise {@code false}.
*/
public static boolean checkPermission(@NonNull Context context, @NonNull String permission) {
return checkPermissions(context, new String[]{permission});
}
/**
* Check if app has been granted the required permissions.
*
* @param context The context for operations.
* @param permissions The {@link String[]} names for permissions to check.
* @return Returns {@code true} if permissions are granted, otherwise {@code false}.
*/
public static boolean checkPermissions(@NonNull Context context, @NonNull String[] permissions) {
// checkSelfPermission may return true for permissions not even requested
List<String> permissionsNotRequested = getPermissionsNotRequested(context, permissions);
if (permissionsNotRequested.size() > 0) {
Log.e(LOG_TAG, "Attempted to check for permissions that have not been requested in app manifest: " + permissionsNotRequested);
return false;
}
for (String permission : permissions) {
if (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_DENIED) {
return false;
}
}
return true;
}
public static boolean requestPermission(@NonNull Context context, @NonNull String permission, int requestCode) {
return requestPermissions(context, new String[]{permission}, requestCode);
}
public static boolean requestPermissions(@NonNull Context context, @NonNull String[] permissions, int requestCode) {
List<String> permissionsNotRequested = getPermissionsNotRequested(context, permissions);
if (permissionsNotRequested.size() > 0) {
throw new RuntimeException("Requested permissions not in the manifest: " + permissionsNotRequested);
}
for (String permission : permissions) {
int result = ContextCompat.checkSelfPermission(context, permission);
if (result != PackageManager.PERMISSION_GRANTED) {
Log.i(LOG_TAG, "Requesting Permissions: " + Arrays.toString(permissions));
((Activity) context).requestPermissions(permissions, requestCode);
break;
}
}
return true;
}
/**
* Check if app has requested the required permission in the manifest.
*
* @param context The context for operations.
* @param permission The {@link String} name for permission to check.
* @return Returns {@code true} if permission has been requested, otherwise {@code false}.
*/
public static boolean isPermissionRequested(@NonNull Context context, @NonNull String permission) {
return getPermissionsNotRequested(context, new String[]{permission}).size() == 0;
}
/**
* Check if app has requested the required permissions or not in the manifest.
*
* @param context The context for operations.
* @param permissions The {@link String[]} names for permissions to check.
* @return Returns {@link List<String>} of permissions that have not been requested. It will have
* size 0 if all permissions have been requested.
*/
@NonNull
public static List<String> getPermissionsNotRequested(@NonNull Context context, @NonNull String[] permissions) {
List<String> permissionsNotRequested = new ArrayList<>();
Collections.addAll(permissionsNotRequested, permissions);
PackageInfo packageInfo;
try {
packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS);
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException(e);
}
// If no permissions are requested, then nothing to check
if (packageInfo.requestedPermissions == null || packageInfo.requestedPermissions.length == 0)
return permissionsNotRequested;
List<String> requestedPermissionsList = Arrays.asList(packageInfo.requestedPermissions);
for (String permission : permissions) {
if (requestedPermissionsList.contains(permission)) {
permissionsNotRequested.remove(permission);
}
}
return permissionsNotRequested;
}
public static boolean checkStoragePermission(@NonNull Context context, boolean checkLegacyStoragePermission) {
if (checkLegacyStoragePermission || Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
return checkPermissions(context,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE});
} else {
return Environment.isExternalStorageManager();
}
}
/**
* Check if {@link Manifest.permission#REQUEST_IGNORE_BATTERY_OPTIMIZATIONS} permission has been
* granted.
*
* @param context The context for operations.
* @return Returns {@code true} if permission is granted, otherwise {@code false}.
*/
public static boolean checkIfBatteryOptimizationsDisabled(@NonNull Context context) {
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
return powerManager.isIgnoringBatteryOptimizations(context.getPackageName());
}
@SuppressLint("BatteryLife")
public static void requestDisableBatteryOptimizations(@NonNull Context context) {
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:" + context.getPackageName()));
// Flag must not be passed for activity contexts, otherwise onActivityResult() will not be called with permission grant result.
// Flag must be passed for non-activity contexts like services, otherwise "Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag" exception will be raised.
if (!(context instanceof Activity))
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
}

View File

@ -0,0 +1,87 @@
package com.termux.app;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.TypedValue;
public class TermuxPreferences {
private static final int MAX_FONTSIZE = 256;
private static final int MIN_FONTSIZE = 256;
private static final String PREF_KEEP_SCREEN_ON = "screen_on";
private static final String PREF_CURRENT_SESSION = "current_session";
private static final String PREF_FONT_SIZE = "font_size";
private static final String PREF_SHOW_TOOLBAR = "show_toolbar";
private int minFontSize;
private int maxFontSize;
private int defaultFontSize;
private final SharedPreferences prefs;
TermuxPreferences(TermuxActivity activity) {
prefs = activity.getPreferences(Context.MODE_PRIVATE);
setupFontSizeDefaults(activity);
}
public void setKeepScreenOn(boolean newValue) {
prefs.edit().putBoolean(PREF_KEEP_SCREEN_ON, newValue).apply();
}
public boolean isKeepScreenOn() {
return prefs.getBoolean(PREF_KEEP_SCREEN_ON, false);
}
public void setCurrentSession(String currentSession) {
prefs.edit().putString(PREF_CURRENT_SESSION, currentSession).apply();
}
public String getCurrentSession() {
return prefs.getString(PREF_KEEP_SCREEN_ON, null);
}
public void setShowTerminalToolbar(boolean newValue) {
prefs.edit().putBoolean(PREF_SHOW_TOOLBAR, newValue).apply();
}
public boolean isShowTerminalToolbar() {
return prefs.getBoolean(PREF_SHOW_TOOLBAR, true);
}
public boolean toggleShowTerminalToolbar() {
boolean newValue = !isShowTerminalToolbar();
prefs.edit().putBoolean(PREF_SHOW_TOOLBAR, newValue).apply();
return newValue;
}
public int getFontSize() {
return prefs.getInt(PREF_FONT_SIZE, defaultFontSize);
}
public int changeFontSize(boolean increase) {
int fontSize = getFontSize();
fontSize += (increase ? 1 : -1) * 2;
fontSize = Math.max(MIN_FONTSIZE, Math.min(fontSize, MAX_FONTSIZE));
prefs.edit().putInt(PREF_FONT_SIZE, fontSize).apply();
return fontSize;
}
private void setupFontSizeDefaults(Context context) {
float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, context.getResources().getDisplayMetrics());
// This is a bit arbitrary and sub-optimal. We want to give a sensible default for minimum font size
// to prevent invisible text due to zoom be mistake:
minFontSize = (int) (4f * dipInPixels);
maxFontSize = 256;
// http://www.google.com/design/spec/style/typography.html#typography-line-height
defaultFontSize = Math.round(12 * dipInPixels);
// Make it divisible by 2 since that is the minimal adjustment step:
if (defaultFontSize % 2 == 1) defaultFontSize--;
}
}

View File

@ -0,0 +1,55 @@
package com.termux.app;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;
public class TermuxProperties {
private final Properties properties = new Properties();
void reloadProperties() {
properties.clear();
try {
for (String subPath : new String[]{".termux.properties", ".config/termux.properties"}) {
File propertiesFile = new File(TermuxConstants.HOME_PATH + '/' + subPath);
if (propertiesFile.exists()) {
try (FileInputStream in = new FileInputStream(propertiesFile)) {
try {
properties.load(in);
} catch (Exception e) {
Log.e(TermuxConstants.LOG_TAG, "Error reading termux properties", e);
// TODO: Show toast
}
}
}
}
} catch (IOException e) {
Log.e(TermuxConstants.LOG_TAG, "Failed to reload properties", e);
}
}
boolean isBackKeyTheEscapeKey() {
return properties.getProperty("back-key", "escape").equalsIgnoreCase("escape");
}
boolean isEnforcingCharBasedInput() {
return properties.getProperty("enforce-char-based-input", "false").equalsIgnoreCase("true");
}
boolean areVirtualVolumeKeysDisabled() {
return false; // TODO
}
public String getExtraKeys() {
String defaultValue = "[['ESC','/',{key: '-', popup: '|'},'HOME','UP','END','PGUP'], ['TAB','CTRL','ALT','LEFT','DOWN','RIGHT','PGDN']]";
return properties.getProperty("extra-keys", defaultValue);
}
public String getExtraKeysStyle() {
return properties.getProperty("extra-keys-style", "default");
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,105 @@
package com.termux.app;
import android.os.Build;
import android.os.Process;
import androidx.annotation.NonNull;
import com.termux.terminal.TerminalSession;
import com.termux.terminal.TerminalSessionClient;
import java.io.File;
public class TermuxSession {
public static final String[] LOGIN_SHELL_BINARIES = new String[]{"login", "bash", "zsh", "fish", "sh"};
public final TerminalSession mTerminalSession;
private final TermuxService mTermuxService;
private TermuxSession(@NonNull final TerminalSession terminalSession, final TermuxService termuxService) {
this.mTerminalSession = terminalSession;
this.mTermuxService = termuxService;
}
public static TermuxSession execute(@NonNull final TerminalSessionClient terminalSessionClient,
final TermuxService termuxSessionClient,
boolean failSafe) {
String executable = null;
if (!failSafe) {
for (String shellBinary : LOGIN_SHELL_BINARIES) {
File shellFile = new File(com.termux.app.TermuxConstants.BIN_PATH, shellBinary);
if (shellFile.canExecute()) {
executable = shellFile.getAbsolutePath();
break;
}
}
}
boolean isLoginShell = false;
if (executable == null) {
// Fall back to system shell as last resort:
// Do not start a login shell since ~/.profile may cause startup failure if its invalid.
// /system/bin/sh is provided by mksh (not toybox) and does load .mkshrc but for android its set
// to /system/etc/mkshrc even though its default is ~/.mkshrc.
// So /system/etc/mkshrc must still be valid for failsafe session to start properly.
// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:external/mksh/src/main.c;l=663
// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:external/mksh/src/main.c;l=41
// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:external/mksh/Android.bp;l=114
executable = "/system/bin/sh";
} else {
isLoginShell = true;
}
String[] commandArgs = TermuxShellUtils.setupShellCommandArguments(executable, new String[0]);
executable = commandArgs[0];
String processName = (isLoginShell ? "-" : "") + new File(executable).getName();
String[] arguments = new String[commandArgs.length];
arguments[0] = processName;
if (commandArgs.length > 1) {
System.arraycopy(commandArgs, 1, arguments, 1, commandArgs.length - 1);
}
if (!failSafe) {
// Cannot execute written files directly on Android 10 or later.
String wrappedExecutable = executable;
executable = "/system/bin/linker" + (Process.is64Bit() ? "64" : "");
String[] origArguments = arguments;
arguments = new String[commandArgs.length + 2];
arguments[0] = processName;
arguments[1] = TermuxConstants.BIN_PATH + "/sh";
arguments[2] = wrappedExecutable;
if (origArguments.length > 1) {
System.arraycopy(origArguments, 1, arguments, 3, origArguments.length - 1);
}
}
// Setup command environment
String[] environmentArray = TermuxShellUtils.setupEnvironment(failSafe);
TerminalSession terminalSession = new TerminalSession(
executable,
com.termux.app.TermuxConstants.HOME_PATH,
arguments,
environmentArray,
4000,
terminalSessionClient
);
return new TermuxSession(terminalSession, termuxSessionClient);
}
public void finish() {
// If process is still running, then ignore the call
if (mTerminalSession.isRunning()) return;
mTermuxService.onTermuxSessionExited(this);
}
public void killIfExecuting() {
// Send SIGKILL to process
mTerminalSession.finishIfRunning();
}
}

View File

@ -1,4 +1,4 @@
package com.termux.app.terminal;
package com.termux.app;
import android.annotation.SuppressLint;
import android.graphics.Color;
@ -16,13 +16,8 @@ import android.widget.ArrayAdapter;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import com.termux.R;
import com.termux.app.TermuxActivity;
import com.termux.shared.termux.shell.command.runner.terminal.TermuxSession;
import com.termux.shared.theme.NightMode;
import com.termux.shared.theme.ThemeUtils;
import com.termux.terminal.TerminalSession;
import java.util.List;
@ -51,20 +46,12 @@ public class TermuxSessionsListViewController extends ArrayAdapter<TermuxSession
TextView sessionTitleView = sessionRowView.findViewById(R.id.session_title);
TerminalSession sessionAtRow = getItem(position).getTerminalSession();
TerminalSession sessionAtRow = getItem(position).mTerminalSession;
if (sessionAtRow == null) {
sessionTitleView.setText("null session");
return sessionRowView;
}
boolean shouldEnableDarkTheme = ThemeUtils.shouldEnableDarkTheme(mActivity, NightMode.getAppNightMode().getName());
if (shouldEnableDarkTheme) {
sessionTitleView.setBackground(
ContextCompat.getDrawable(mActivity, R.drawable.session_background_black_selected)
);
}
String name = sessionAtRow.mSessionName;
String sessionTitle = sessionAtRow.getTitle();
@ -86,7 +73,7 @@ public class TermuxSessionsListViewController extends ArrayAdapter<TermuxSession
} else {
sessionTitleView.setPaintFlags(sessionTitleView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
}
int defaultColor = shouldEnableDarkTheme ? Color.WHITE : Color.BLACK;
int defaultColor = Color.WHITE;
int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? defaultColor : Color.RED;
sessionTitleView.setTextColor(color);
return sessionRowView;
@ -95,14 +82,14 @@ public class TermuxSessionsListViewController extends ArrayAdapter<TermuxSession
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
TermuxSession clickedSession = getItem(position);
mActivity.getTermuxTerminalSessionClient().setCurrentSession(clickedSession.getTerminalSession());
mActivity.getTermuxTerminalSessionClient().setCurrentSession(clickedSession.mTerminalSession);
mActivity.getDrawer().closeDrawers();
}
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
final TermuxSession selectedSession = getItem(position);
mActivity.getTermuxTerminalSessionClient().renameSession(selectedSession.getTerminalSession());
mActivity.getTermuxTerminalSessionClient().renameSession(selectedSession.mTerminalSession);
return true;
}

View File

@ -0,0 +1,37 @@
package com.termux.app;
import android.content.Context;
import android.widget.ArrayAdapter;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.List;
public class TermuxShellManager {
private static int SHELL_ID = 0;
protected final Context mContext;
/**
* The foreground TermuxSessions which this service manages.
* Note that this list is observed by an activity, like TermuxActivity.mTermuxSessionListViewController,
* so any changes must be made on the UI thread and followed by a call to
* {@link ArrayAdapter#notifyDataSetChanged()}.
*/
public final List<TermuxSession> mTermuxSessions = new ArrayList<>();
/**
* The background TermuxTasks which this service manages.
*/
public final List<TermuxAppShell> mTermuxTasks = new ArrayList<>();
public TermuxShellManager(@NonNull Context context) {
mContext = context.getApplicationContext();
}
public static synchronized int getNextShellId() {
return SHELL_ID++;
}
}

View File

@ -1,32 +1,20 @@
package com.termux.shared.termux.shell;
package com.termux.app;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.termux.shared.errors.Error;
import com.termux.shared.file.filesystem.FileTypes;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.file.FileUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.termux.settings.properties.TermuxAppSharedProperties;
import org.apache.commons.io.filefilter.TrueFileFilter;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class TermuxShellUtils {
private static final String LOG_TAG = "TermuxShellUtils";
/**
* Setup shell command arguments for the execute. The file interpreter may be prefixed to
* command arguments if needed.
*/
@NonNull
public static String[] setupShellCommandArguments(@NonNull String executable, @Nullable String[] arguments) {
// The file to execute may either be:
@ -57,7 +45,7 @@ public class TermuxShellUtils {
if (shebangExecutable.startsWith("/usr") || shebangExecutable.startsWith("/bin")) {
String[] parts = shebangExecutable.split("/");
String binary = parts[parts.length - 1];
interpreter = TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/" + binary;
interpreter = TermuxConstants.BIN_PATH + "/" + binary;
}
break;
}
@ -67,7 +55,7 @@ public class TermuxShellUtils {
}
} else {
// No shebang and no ELF, use standard shell.
interpreter = TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/sh";
interpreter = TermuxConstants.BIN_PATH + "/sh";
}
}
}
@ -82,41 +70,68 @@ public class TermuxShellUtils {
return result.toArray(new String[0]);
}
/** Clear files under {@link TermuxConstants#TERMUX_TMP_PREFIX_DIR_PATH}. */
public static void clearTermuxTMPDIR(boolean onlyIfExists) {
// Existence check before clearing may be required since clearDirectory() will automatically
// re-create empty directory if doesn't exist, which should not be done for things like
// termux-reset (d6eb5e35). Moreover, TMPDIR must be a directory and not a symlink, this can
// also allow users who don't want TMPDIR to be cleared automatically on termux exit, since
// it may remove files still being used by background processes (#1159).
if(onlyIfExists && !FileUtils.directoryFileExists(TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH, false))
return;
Error error;
public static String[] setupEnvironment(boolean failsafe) {
Map<String, String> environment = new HashMap<>();
environment.put("HOME", TermuxConstants.HOME_PATH);
environment.put("LANG", "en_US.UTF-8");
String tmpDir = TermuxConstants.PREFIX_PATH + "/tmp";
environment.put("TMP", tmpDir);
environment.put("TMPDIR", tmpDir);
environment.put("COLORTERM", "truecolor");
environment.put("TERM", "xterm-256color");
putToEnvIfInSystemEnv(environment, "PATH");
putToEnvIfInSystemEnv(environment, "ANDROID_ASSETS");
putToEnvIfInSystemEnv(environment, "ANDROID_DATA");
putToEnvIfInSystemEnv(environment, "ANDROID_ROOT");
putToEnvIfInSystemEnv(environment, "ANDROID_STORAGE");
// EXTERNAL_STORAGE is needed for /system/bin/am to work on at least
// Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3.
// https://cs.android.com/android/_/android/platform/system/core/+/fc000489
putToEnvIfInSystemEnv(environment, "EXTERNAL_STORAGE");
putToEnvIfInSystemEnv(environment, "ASEC_MOUNTPOINT");
putToEnvIfInSystemEnv(environment, "LOOP_MOUNTPOINT");
putToEnvIfInSystemEnv(environment, "ANDROID_RUNTIME_ROOT");
putToEnvIfInSystemEnv(environment, "ANDROID_ART_ROOT");
putToEnvIfInSystemEnv(environment, "ANDROID_I18N_ROOT");
putToEnvIfInSystemEnv(environment, "ANDROID_TZDATA_ROOT");
putToEnvIfInSystemEnv(environment, "BOOTCLASSPATH");
putToEnvIfInSystemEnv(environment, "DEX2OATBOOTCLASSPATH");
putToEnvIfInSystemEnv(environment, "SYSTEMSERVERCLASSPATH");
TermuxAppSharedProperties properties = TermuxAppSharedProperties.getProperties();
int days = properties.getDeleteTMPDIRFilesOlderThanXDaysOnExit();
if (!failsafe) {
environment.put("LD_PRELOAD", TermuxConstants.PREFIX_PATH + "/lib/libtermux-exec.so");
environment.put("PATH", TermuxConstants.PREFIX_PATH + "/bin:" + System.getenv("PATH"));
}
// Disable currently until FileUtils.deleteFilesOlderThanXDays() is fixed.
if (days > 0)
days = 0;
List<String> environmentList = new ArrayList<>(environment.values());
for (Map.Entry<String, String> entry : environment.entrySet()) {
environmentList.add(entry.getKey() + "=" + entry.getValue());
}
Collections.sort(environmentList);
return environmentList.toArray(new String[0]);
}
if (days < 0) {
Logger.logInfo(LOG_TAG, "Not clearing termux $TMPDIR");
} else if (days == 0) {
error = FileUtils.clearDirectory("$TMPDIR",
FileUtils.getCanonicalPath(TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH, null));
if (error != null) {
Logger.logErrorExtended(LOG_TAG, "Failed to clear termux $TMPDIR\n" + error);
}
} else {
error = FileUtils.deleteFilesOlderThanXDays("$TMPDIR",
FileUtils.getCanonicalPath(TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH, null),
TrueFileFilter.INSTANCE, days, true, FileTypes.FILE_TYPE_ANY_FLAGS);
if (error != null) {
Logger.logErrorExtended(LOG_TAG, "Failed to delete files from termux $TMPDIR older than " + days + " days\n" + error);
}
private static void putToEnvIfInSystemEnv(@NonNull Map<String, String> environment, @NonNull String name) {
String value = System.getenv(name);
if (value != null) {
environment.put(name, value);
}
}
public static int getPid(Process p) {
try {
Field f = p.getClass().getDeclaredField("pid");
f.setAccessible(true);
try {
return f.getInt(p);
} finally {
f.setAccessible(false);
}
} catch (Throwable e) {
return -1;
}
}
}

View File

@ -1,32 +1,28 @@
package com.termux.app.terminal;
package com.termux.app;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.Typeface;
import android.media.AudioAttributes;
import android.media.SoundPool;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.text.TextUtils;
import android.util.Log;
import android.widget.ListView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.termux.R;
import com.termux.shared.interact.ShareUtils;
import com.termux.shared.termux.shell.command.runner.terminal.TermuxSession;
import com.termux.shared.termux.interact.TextInputDialogUtils;
import com.termux.app.TermuxActivity;
import com.termux.shared.termux.terminal.TermuxTerminalSessionClientBase;
import com.termux.shared.termux.TermuxConstants;
import com.termux.app.TermuxService;
import com.termux.shared.termux.settings.properties.TermuxPropertyConstants;
import com.termux.shared.termux.terminal.io.BellHandler;
import com.termux.shared.logger.Logger;
import com.termux.terminal.TerminalColors;
import com.termux.terminal.TerminalSession;
import com.termux.terminal.TerminalSessionClient;
@ -37,8 +33,77 @@ import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Properties;
/** The {@link TerminalSessionClient} implementation that may require an {@link Activity} for its interface methods. */
public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionClientBase {
/**
* The {@link TerminalSessionClient} implementation that may require an {@link Activity} for its interface methods.
*/
public final class TermuxTerminalSessionActivityClient implements TerminalSessionClient {
public static class BellHandler {
private static BellHandler instance = null;
private static final Object lock = new Object();
private static final String LOG_TAG = "BellHandler";
public static BellHandler getInstance(Context context) {
if (instance == null) {
synchronized (lock) {
if (instance == null) {
instance = new BellHandler((Vibrator) context.getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE));
}
}
}
return instance;
}
private static final long DURATION = 50;
private static final long MIN_PAUSE = 3 * DURATION;
private final Handler handler = new Handler(Looper.getMainLooper());
private long lastBell = 0;
private final Runnable bellRunnable;
private BellHandler(final Vibrator vibrator) {
bellRunnable = new Runnable() {
@Override
public void run() {
if (vibrator != null) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(VibrationEffect.createOneShot(DURATION, VibrationEffect.DEFAULT_AMPLITUDE));
} else {
vibrator.vibrate(DURATION);
}
} catch (Exception e) {
// Issue on samsung devices on android 8
// java.lang.NullPointerException: Attempt to read from field 'android.os.VibrationEffect com.android.server.VibratorService$Vibration.mEffect' on a null object reference
Log.e(LOG_TAG, "Failed to run vibrator", e);
}
}
}
};
}
public synchronized void doBell() {
long now = now();
long timeSinceLastBell = now - lastBell;
if (timeSinceLastBell < 0) {
// there is a next bell pending; don't schedule another one
} else if (timeSinceLastBell < MIN_PAUSE) {
// there was a bell recently, schedule the next one
handler.postDelayed(bellRunnable, MIN_PAUSE - timeSinceLastBell);
lastBell = lastBell + MIN_PAUSE;
} else {
// the last bell was long ago, do it now
bellRunnable.run();
lastBell = now;
}
}
private long now() {
return SystemClock.uptimeMillis();
}
}
private final TermuxActivity mActivity;
@ -63,7 +128,7 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
}
/**
* Should be called when mActivity.onStart() is called
* Called when mActivity.onStart() is called
*/
public void onStart() {
// The service has connected, but data may have changed since we were last in the foreground.
@ -112,13 +177,11 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
checkForFontAndColors();
}
@Override
public void onTextChanged(@NonNull TerminalSession changedSession) {
if (!mActivity.isVisible()) return;
if (mActivity.getCurrentSession() == changedSession) mActivity.getTerminalView().onScreenUpdated();
if (mActivity.isVisible() && mActivity.getCurrentSession() == changedSession) {
mActivity.getTerminalView().onScreenUpdated();
}
}
@Override
@ -150,13 +213,7 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
// For plugin commands that expect the result back, we should immediately close the session
// and send the result back instead of waiting fo the user to press enter.
// The plugin can handle/show errors itself.
boolean isPluginExecutionCommandWithPendingResult = false;
TermuxSession termuxSession = service.getTermuxSession(index);
if (termuxSession != null) {
isPluginExecutionCommandWithPendingResult = termuxSession.getExecutionCommand().isPluginExecutionCommandWithPendingResult();
if (isPluginExecutionCommandWithPendingResult)
Logger.logVerbose(LOG_TAG, "The \"" + finishedSession.mSessionName + "\" session will be force finished automatically since result in pending.");
}
if (mActivity.isVisible() && finishedSession != mActivity.getCurrentSession()) {
// Show toast for non-current sessions that exit.
@ -168,13 +225,13 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
if (mActivity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
// On Android TV devices we need to use older behaviour because we may
// not be able to have multiple launcher icons.
if (service.getTermuxSessionsSize() > 1 || isPluginExecutionCommandWithPendingResult) {
if (service.getTermuxSessionsSize() > 1) {
removeFinishedSession(finishedSession);
}
} else {
// Once we have a separate launcher icon for the failsafe session, it
// should be safe to auto-close session on exit code '0' or '130'.
if (finishedSession.getExitStatus() == 0 || finishedSession.getExitStatus() == 130 || isPluginExecutionCommandWithPendingResult) {
if (finishedSession.getExitStatus() == 0 || finishedSession.getExitStatus() == 130) {
removeFinishedSession(finishedSession);
}
}
@ -183,15 +240,14 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
@Override
public void onCopyTextToClipboard(@NonNull TerminalSession session, String text) {
if (!mActivity.isVisible()) return;
ShareUtils.copyTextToClipboard(mActivity, text);
TermuxUrlUtils.copyTextToClipboard(mActivity, text);
}
@Override
public void onPasteTextFromClipboard(@Nullable TerminalSession session) {
if (!mActivity.isVisible()) return;
String text = ShareUtils.getTextStringFromClipboardIfSet(mActivity, true);
String text = TermuxUrlUtils.getTextStringFromClipboardIfSet(mActivity, true);
if (text != null)
mActivity.getTerminalView().mEmulator.paste(text);
}
@ -200,18 +256,10 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
public void onBell(@NonNull TerminalSession session) {
if (!mActivity.isVisible()) return;
switch (mActivity.getProperties().getBellBehaviour()) {
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_VIBRATE:
BellHandler.getInstance(mActivity).doBell();
break;
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_BEEP:
loadBellSoundPool();
if (mBellSoundPool != null)
mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
break;
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_IGNORE:
// Ignore the bell character.
break;
//BellHandler.getInstance(mActivity).doBell();
loadBellSoundPool();
if (mBellSoundPool != null) {
mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
}
}
@ -222,48 +270,13 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
}
@Override
public void onTerminalCursorStateChange(boolean enabled) {
// Do not start cursor blinking thread if activity is not visible
if (enabled && !mActivity.isVisible()) {
Logger.logVerbose(LOG_TAG, "Ignoring call to start cursor blinking since activity is not visible");
return;
}
public void onTerminalCursorStateChange(boolean state) {
// If cursor is to enabled now, then start cursor blinking if blinking is enabled
// otherwise stop cursor blinking
mActivity.getTerminalView().setTerminalCursorBlinkerState(enabled, false);
}
@Override
public void setTerminalShellPid(@NonNull TerminalSession terminalSession, int pid) {
TermuxService service = mActivity.getTermuxService();
if (service == null) return;
TermuxSession termuxSession = service.getTermuxSessionForTerminalSession(terminalSession);
if (termuxSession != null)
termuxSession.getExecutionCommand().mPid = pid;
}
/**
* Should be called when mActivity.onResetTerminalSession() is called
* Load mBellSoundPool
*/
public void onResetTerminalSession() {
// Ensure blinker starts again after reset if cursor blinking was disabled before reset like
// with "tput civis" which would have called onTerminalCursorStateChange()
mActivity.getTerminalView().setTerminalCursorBlinkerState(true, true);
}
@Override
public Integer getTerminalCursorStyle() {
return mActivity.getProperties().getTerminalCursorStyle();
}
/** Load mBellSoundPool */
private synchronized void loadBellSoundPool() {
if (mBellSoundPool == null) {
mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
@ -271,15 +284,17 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
try {
mBellSoundId = mBellSoundPool.load(mActivity, R.raw.bell, 1);
} catch (Exception e){
mBellSoundId = mBellSoundPool.load(mActivity, com.termux.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);
Log.e(LOG_TAG, "Failed to load bell sound pool", e);
}
}
}
/** Release mBellSoundPool resources */
/**
* Release mBellSoundPool resources
*/
private synchronized void releaseBellSoundPool() {
if (mBellSoundPool != null) {
mBellSoundPool.release();
@ -288,11 +303,10 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
}
/** Try switching to session. */
/**
* Try switching to session.
*/
public void setCurrentSession(TerminalSession session) {
if (session == null) return;
if (mActivity.getTerminalView().attachSession(session)) {
// notify about switched session if not already displaying the session
notifyOfSessionChange();
@ -306,11 +320,8 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
void notifyOfSessionChange() {
if (!mActivity.isVisible()) return;
if (!mActivity.getProperties().areTerminalSessionChangeToastsDisabled()) {
TerminalSession session = mActivity.getCurrentSession();
mActivity.showToast(toToastTitle(session), false);
}
TerminalSession session = mActivity.getCurrentSession();
mActivity.showToast(toToastTitle(session), false);
}
public void switchToSession(boolean forward) {
@ -328,7 +339,7 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
TermuxSession termuxSession = service.getTermuxSession(index);
if (termuxSession != null)
setCurrentSession(termuxSession.getTerminalSession());
setCurrentSession(termuxSession.mTerminalSession);
}
public void switchToSession(int index) {
@ -337,14 +348,14 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
TermuxSession termuxSession = service.getTermuxSession(index);
if (termuxSession != null)
setCurrentSession(termuxSession.getTerminalSession());
setCurrentSession(termuxSession.mTerminalSession);
}
@SuppressLint("InflateParams")
public void renameSession(final TerminalSession sessionToRename) {
if (sessionToRename == null) return;
TextInputDialogUtils.textInput(mActivity, R.string.title_rename_session, sessionToRename.mSessionName, R.string.action_rename_session_confirm, text -> {
TermuxMessageDialogUtils.textInput(mActivity, R.string.title_rename_session, sessionToRename.mSessionName, R.string.action_rename_session_confirm, text -> {
renameSession(sessionToRename, text);
termuxSessionListNotifyUpdated();
}, -1, null, -1, null, null);
@ -353,35 +364,28 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
private void renameSession(TerminalSession sessionToRename, String text) {
if (sessionToRename == null) return;
sessionToRename.mSessionName = text;
TermuxService service = mActivity.getTermuxService();
if (service != null) {
TermuxSession termuxSession = service.getTermuxSessionForTerminalSession(sessionToRename);
if (termuxSession != null)
termuxSession.getExecutionCommand().shellName = text;
}
}
public void addNewSession(boolean isFailSafe, String sessionName) {
TermuxService service = mActivity.getTermuxService();
if (service == null) return;
if (service == null) {
return;
}
if (service.getTermuxSessionsSize() >= MAX_SESSIONS) {
new AlertDialog.Builder(mActivity).setTitle(R.string.title_max_terminals_reached).setMessage(R.string.msg_max_terminals_reached)
.setPositiveButton(android.R.string.ok, null).show();
new AlertDialog.Builder(mActivity)
.setTitle(R.string.title_max_terminals_reached)
.setMessage(R.string.msg_max_terminals_reached)
.setPositiveButton(android.R.string.ok, null)
.show();
} else {
TerminalSession currentSession = mActivity.getCurrentSession();
String workingDirectory;
if (currentSession == null) {
workingDirectory = mActivity.getProperties().getDefaultWorkingDirectory();
} else {
workingDirectory = currentSession.getCwd();
}
String workingDirectory = currentSession == null ? TermuxConstants.HOME_PATH : currentSession.getCwd();
TermuxSession newTermuxSession = service.createTermuxSession(null, null, null, workingDirectory, isFailSafe, sessionName);
if (newTermuxSession == null) return;
TerminalSession newTerminalSession = newTermuxSession.getTerminalSession();
TerminalSession newTerminalSession = newTermuxSession.mTerminalSession;
setCurrentSession(newTerminalSession);
mActivity.getDrawer().closeDrawers();
@ -390,44 +394,31 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
public void setCurrentStoredSession() {
TerminalSession currentSession = mActivity.getCurrentSession();
if (currentSession != null)
mActivity.getPreferences().setCurrentSession(currentSession.mHandle);
else
mActivity.getPreferences().setCurrentSession(null);
mActivity.mPreferences.setCurrentSession(currentSession == null ? null : currentSession.mHandle);
;
}
/** The current session as stored or the last one if that does not exist. */
/**
* The current session as stored or the last one if that does not exist.
*/
public TerminalSession getCurrentStoredSessionOrLast() {
TerminalSession stored = getCurrentStoredSession();
if (stored != null) {
// If a stored session is in the list of currently running sessions, then return it
return stored;
} else {
// Else return the last session currently running
TermuxService service = mActivity.getTermuxService();
if (service == null) return null;
TermuxSession termuxSession = service.getLastTermuxSession();
if (termuxSession != null)
return termuxSession.getTerminalSession();
else
return null;
}
}
private TerminalSession getCurrentStoredSession() {
String sessionHandle = mActivity.getPreferences().getCurrentSession();
// If no session is stored in shared preferences
if (sessionHandle == null)
String currentSessionHandle = mActivity.mPreferences.getCurrentSession();
if (currentSessionHandle == null) {
return null;
}
// Check if the session handle found matches one of the currently running sessions
TermuxService service = mActivity.getTermuxService();
if (service == null) return null;
return service.getTerminalSessionForHandle(sessionHandle);
TerminalSession currentSession = service.getTerminalSessionForHandle(currentSessionHandle);
if (currentSession == null) {
TermuxSession termuxSession = service.getLastTermuxSession();
return termuxSession == null ? null : termuxSession.mTerminalSession;
} else {
return currentSession;
}
}
public void removeFinishedSession(TerminalSession finishedSession) {
@ -446,8 +437,9 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
index = size - 1;
}
TermuxSession termuxSession = service.getTermuxSession(index);
if (termuxSession != null)
setCurrentSession(termuxSession.getTerminalSession());
if (termuxSession != null) {
setCurrentSession(termuxSession.mTerminalSession);
}
}
}
@ -493,8 +485,8 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
public void checkForFontAndColors() {
try {
File colorsFile = TermuxConstants.TERMUX_COLOR_PROPERTIES_FILE;
File fontFile = TermuxConstants.TERMUX_FONT_FILE;
File fontFile = new File(TermuxConstants.HOME_PATH + "/.termux/font.ttf");
File colorsFile = new File(TermuxConstants.HOME_PATH + "/.termux/colors.properties");
final Properties props = new Properties();
if (colorsFile.isFile()) {
@ -513,7 +505,7 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl
final Typeface newTypeface = (fontFile.exists() && fontFile.length() > 0) ? Typeface.createFromFile(fontFile) : Typeface.MONOSPACE;
mActivity.getTerminalView().setTypeface(newTypeface);
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Error in checkForFontAndColors()", e);
Log.e(LOG_TAG, "Error in checkForFontAndColors()", e);
}
}

View File

@ -0,0 +1,413 @@
package com.termux.app;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.Context;
import android.media.AudioManager;
import android.util.Log;
import android.view.Gravity;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.inputmethod.InputMethodManager;
import android.widget.ListView;
import com.termux.R;
import com.termux.app.extrakeys.SpecialButton;
import com.termux.terminal.KeyHandler;
import com.termux.terminal.TerminalBuffer;
import com.termux.terminal.TerminalEmulator;
import com.termux.terminal.TerminalSession;
import com.termux.view.TerminalViewClient;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import androidx.drawerlayout.widget.DrawerLayout;
public final class TermuxTerminalViewClient implements TerminalViewClient {
final TermuxActivity mActivity;
final TermuxTerminalSessionActivityClient mTermuxTerminalSessionActivityClient;
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
private static final String LOG_TAG = "TermuxTerminalViewClient";
public TermuxTerminalViewClient(TermuxActivity activity, TermuxTerminalSessionActivityClient termuxTerminalSessionActivityClient) {
this.mActivity = activity;
this.mTermuxTerminalSessionActivityClient = termuxTerminalSessionActivityClient;
}
public TermuxActivity getActivity() {
return mActivity;
}
/**
* Should be called when mActivity.onStart() is called
*/
public void onStart() {
// Piggyback on the terminal view key logging toggle for now, should add a separate toggle in future
}
@Override
public float onScale(float scale) {
if (scale < 0.9f || scale > 1.1f) {
boolean increase = scale > 1.f;
changeFontSize(increase);
return 1.0f;
}
return scale;
}
@Override
public void onSingleTapUp(MotionEvent e) {
TerminalSession session = mActivity.getCurrentSession();
if (session != null) {
TerminalEmulator term = mActivity.getCurrentSession().getEmulator();
if (!term.isMouseTrackingActive() && !e.isFromSource(InputDevice.SOURCE_MOUSE)) {
mActivity.getSystemService(InputMethodManager.class).showSoftInput(mActivity.getTerminalView(), 0);
}
}
}
@Override
public boolean shouldBackButtonBeMappedToEscape() {
return mActivity.mProperties.isBackKeyTheEscapeKey();
}
@Override
public boolean shouldEnforceCharBasedInput() {
return mActivity.mProperties.isEnforcingCharBasedInput();
}
@Override
public boolean isTerminalViewSelected() {
return mActivity.getTerminalToolbarViewPager() == null || mActivity.isTerminalViewSelected() || mActivity.getTerminalView().hasFocus();
}
@Override
public void copyModeChanged(boolean copyMode) {
// Disable drawer while copying.
mActivity.getDrawer().setDrawerLockMode(copyMode ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED : DrawerLayout.LOCK_MODE_UNLOCKED);
}
@SuppressLint("RtlHardcoded")
@Override
public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession currentSession) {
if (handleVirtualKeys(keyCode, e, true)) return true;
if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) {
mTermuxTerminalSessionActivityClient.removeFinishedSession(currentSession);
return true;
} else if (e.isCtrlPressed() && e.isAltPressed()) {
// Get the unmodified code point:
int unicodeChar = e.getUnicodeChar(0);
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || unicodeChar == 'n'/* next */) {
mTermuxTerminalSessionActivityClient.switchToSession(true);
} else if (keyCode == KeyEvent.KEYCODE_DPAD_UP || unicodeChar == 'p' /* previous */) {
mTermuxTerminalSessionActivityClient.switchToSession(false);
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
mActivity.getDrawer().openDrawer(Gravity.LEFT);
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
mActivity.getDrawer().closeDrawers();
} else if (unicodeChar == 'c'/* create */) {
mTermuxTerminalSessionActivityClient.addNewSession(false, null);
} else if (unicodeChar == 'k'/* keyboard */) {
onToggleSoftKeyboardRequest();
} else if (unicodeChar == 'm'/* menu */) {
mActivity.getTerminalView().showContextMenu();
} else if (unicodeChar == 'r'/* rename */) {
mTermuxTerminalSessionActivityClient.renameSession(currentSession);
} else if (unicodeChar == 'u' /* urls */) {
showUrlSelection();
} else if (unicodeChar == 'v') {
doPaste();
} 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);
} else if (unicodeChar >= '1' && unicodeChar <= '9') {
int index = unicodeChar - '1';
mTermuxTerminalSessionActivityClient.switchToSession(index);
}
return true;
}
return false;
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent e) {
// If emulator is not set, like if bootstrap installation failed and user dismissed the error
// dialog, then just exit the activity, otherwise they will be stuck in a broken state.
if (keyCode == KeyEvent.KEYCODE_BACK && mActivity.getTerminalView().mEmulator == null) {
mActivity.finishActivityIfNotFinishing();
return true;
}
return handleVirtualKeys(keyCode, e, false);
}
/** Handle dedicated volume buttons as virtual keys if applicable. */
private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) {
InputDevice inputDevice = event.getDevice();
if (mActivity.mProperties.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) {
mVirtualControlKeyDown = down;
return true;
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
mVirtualFnKeyDown = down;
return true;
}
return false;
}
@Override
public boolean readControlKey() {
return readExtraKeysSpecialButton(SpecialButton.CTRL) || mVirtualControlKeyDown;
}
@Override
public boolean readAltKey() {
return readExtraKeysSpecialButton(SpecialButton.ALT);
}
@Override
public boolean readShiftKey() {
return readExtraKeysSpecialButton(SpecialButton.SHIFT);
}
@Override
public boolean readFnKey() {
return readExtraKeysSpecialButton(SpecialButton.FN);
}
public boolean readExtraKeysSpecialButton(SpecialButton specialButton) {
if (mActivity.getExtraKeysView() == null) return false;
Boolean state = mActivity.getExtraKeysView().readSpecialButton(specialButton, true);
if (state == null) {
Log.e(LOG_TAG,"Failed to read an unregistered " + specialButton + " special button value from extra keys.");
return false;
}
return state;
}
@Override
public boolean onLongPress(MotionEvent event) {
return false;
}
@Override
public boolean onCodePoint(final int codePoint, boolean ctrlDown, TerminalSession session) {
if (mVirtualFnKeyDown) {
int resultingKeyCode = -1;
int resultingCodePoint = -1;
boolean altDown = false;
int lowerCase = Character.toLowerCase(codePoint);
switch (lowerCase) {
// Arrow keys.
case 'w':
resultingKeyCode = KeyEvent.KEYCODE_DPAD_UP;
break;
case 'a':
resultingKeyCode = KeyEvent.KEYCODE_DPAD_LEFT;
break;
case 's':
resultingKeyCode = KeyEvent.KEYCODE_DPAD_DOWN;
break;
case 'd':
resultingKeyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
break;
// Page up and down.
case 'p':
resultingKeyCode = KeyEvent.KEYCODE_PAGE_UP;
break;
case 'n':
resultingKeyCode = KeyEvent.KEYCODE_PAGE_DOWN;
break;
// Some special keys:
case 't':
resultingKeyCode = KeyEvent.KEYCODE_TAB;
break;
case 'i':
resultingKeyCode = KeyEvent.KEYCODE_INSERT;
break;
case 'h':
resultingCodePoint = '~';
break;
// Special characters to input.
case 'u':
resultingCodePoint = '_';
break;
case 'l':
resultingCodePoint = '|';
break;
// Function keys.
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
resultingKeyCode = (codePoint - '1') + KeyEvent.KEYCODE_F1;
break;
case '0':
resultingKeyCode = KeyEvent.KEYCODE_F10;
break;
// Other special keys.
case 'e':
resultingCodePoint = /*Escape*/ 27;
break;
case '.':
resultingCodePoint = /*^.*/ 28;
break;
case 'b': // alt+b, jumping backward in readline.
case 'f': // alf+f, jumping forward in readline.
case 'x': // alt+x, common in emacs.
resultingCodePoint = lowerCase;
altDown = true;
break;
// Volume control.
case 'v':
resultingCodePoint = -1;
AudioManager audio = (AudioManager) mActivity.getSystemService(Context.AUDIO_SERVICE);
audio.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME, AudioManager.USE_DEFAULT_STREAM_TYPE, AudioManager.FLAG_SHOW_UI);
break;
// Writing mode:
case 'q':
case 'k':
mActivity.toggleTerminalToolbar();
mVirtualFnKeyDown=false; // force disable fn key down to restore keyboard input into terminal view, fixes termux/termux-app#1420
break;
case 'z': // Zecret :)
mActivity.requestAutoFill();
}
if (resultingKeyCode != -1) {
TerminalEmulator term = session.getEmulator();
session.write(KeyHandler.getCode(resultingKeyCode, 0, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode()));
} else if (resultingCodePoint != -1) {
session.writeCodePoint(altDown, resultingCodePoint);
}
return true;
} else if (ctrlDown) {
if (codePoint == 106 /* Ctrl+j or \n */ && !session.isRunning()) {
mTermuxTerminalSessionActivityClient.removeFinishedSession(session);
return true;
}
}
return false;
}
public void changeFontSize(boolean increase) {
int newFontSize = mActivity.mPreferences.changeFontSize(increase);
mActivity.getTerminalView().setTextSize(newFontSize);
}
/**
* Called when user requests the soft keyboard to be toggled via "KEYBOARD" toggle button in
* drawer or extra keys, or with ctrl+alt+k hardware keyboard shortcut.
*/
public void onToggleSoftKeyboardRequest() {
mActivity.getSystemService(InputMethodManager.class).toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
}
public void shareSessionTranscript() {
TerminalSession session = mActivity.getCurrentSession();
if (session == null) return;
TerminalEmulator terminalEmulator = session.getEmulator();
if (terminalEmulator == null) return;
TerminalBuffer terminalBuffer = terminalEmulator.getScreen();
if (terminalBuffer == null) return;
String sessionTranscript = terminalBuffer.getTranscriptTextWithoutJoinedLines().trim();
TermuxUrlUtils.shareText(mActivity, mActivity.getString(R.string.title_share_transcript),
sessionTranscript, mActivity.getString(R.string.title_share_transcript_with));
}
public void shareSelectedText() {
String selectedText = mActivity.getTerminalView().getStoredSelectedText();
if (selectedText != null && !selectedText.isEmpty()) {
TermuxUrlUtils.shareText(mActivity, mActivity.getString(R.string.title_share_selected_text),
selectedText, mActivity.getString(R.string.title_share_selected_text_with));
}
}
public void showUrlSelection() {
TerminalSession session = mActivity.getCurrentSession();
if (session == null) return;
TerminalEmulator terminalEmulator = session.getEmulator();
if (terminalEmulator == null) return;
TerminalBuffer terminalBuffer = terminalEmulator.getScreen();
if (terminalBuffer == null) return;
String sessionTranscript = terminalBuffer.getTranscriptTextWithFullLinesJoined().trim();
LinkedHashSet<CharSequence> urlSet = TermuxUrlUtils.extractUrls(sessionTranscript);
if (urlSet.isEmpty()) {
new AlertDialog.Builder(mActivity).setMessage(R.string.title_select_url_none_found).show();
return;
}
final CharSequence[] urls = urlSet.toArray(new CharSequence[0]);
Collections.reverse(Arrays.asList(urls)); // Latest first.
// Click to copy url to clipboard:
final AlertDialog dialog = new AlertDialog.Builder(mActivity).setItems(urls, (di, which) -> {
String url = (String) urls[which];
TermuxUrlUtils.copyTextToClipboard(mActivity, url, mActivity.getString(R.string.msg_select_url_copied_to_clipboard));
}).setTitle(R.string.title_select_url_dialog).create();
// Long press to open URL:
dialog.setOnShowListener(di -> {
ListView lv = dialog.getListView(); // this is a ListView with your "buds" in it
lv.setOnItemLongClickListener((parent, view, position, id) -> {
dialog.dismiss();
String url = (String) urls[position];
TermuxUrlUtils.openUrl(mActivity, url);
return true;
});
});
dialog.show();
}
public void doPaste() {
TerminalSession session = mActivity.getCurrentSession();
if (session == null || !session.isRunning()) {
return;
}
String text = TermuxUrlUtils.getTextStringFromClipboardIfSet(mActivity, true);
if (text != null) {
session.getEmulator().paste(text);
}
}
}

View File

@ -0,0 +1,235 @@
package com.termux.app;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.Nullable;
import java.util.LinkedHashSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TermuxUrlUtils {
public static Pattern URL_MATCH_REGEX;
public static Pattern getUrlMatchRegex() {
if (URL_MATCH_REGEX != null) return URL_MATCH_REGEX;
StringBuilder regex_sb = new StringBuilder();
regex_sb.append("("); // Begin first matching group.
regex_sb.append("(?:"); // Begin scheme group.
regex_sb.append("dav|"); // The DAV proto.
regex_sb.append("dict|"); // The DICT proto.
regex_sb.append("dns|"); // The DNS proto.
regex_sb.append("file|"); // File path.
regex_sb.append("finger|"); // The Finger proto.
regex_sb.append("ftp(?:s?)|"); // The FTP proto.
regex_sb.append("git|"); // The Git proto.
regex_sb.append("gemini|"); // The Gemini proto.
regex_sb.append("gopher|"); // The Gopher proto.
regex_sb.append("http(?:s?)|"); // The HTTP proto.
regex_sb.append("imap(?:s?)|"); // The IMAP proto.
regex_sb.append("irc(?:[6s]?)|"); // The IRC proto.
regex_sb.append("ip[fn]s|"); // The IPFS proto.
regex_sb.append("ldap(?:s?)|"); // The LDAP proto.
regex_sb.append("pop3(?:s?)|"); // The POP3 proto.
regex_sb.append("redis(?:s?)|"); // The Redis proto.
regex_sb.append("rsync|"); // The Rsync proto.
regex_sb.append("rtsp(?:[su]?)|"); // The RTSP proto.
regex_sb.append("sftp|"); // The SFTP proto.
regex_sb.append("smb(?:s?)|"); // The SAMBA proto.
regex_sb.append("smtp(?:s?)|"); // The SMTP proto.
regex_sb.append("svn(?:(?:\\+ssh)?)|"); // The Subversion proto.
regex_sb.append("tcp|"); // The TCP proto.
regex_sb.append("telnet|"); // The Telnet proto.
regex_sb.append("tftp|"); // The TFTP proto.
regex_sb.append("udp|"); // The UDP proto.
regex_sb.append("vnc|"); // The VNC proto.
regex_sb.append("ws(?:s?)"); // The Websocket proto.
regex_sb.append(")://"); // End scheme group.
regex_sb.append(")"); // End first matching group.
// Begin second matching group.
regex_sb.append("(");
// User name and/or password in format 'user:pass@'.
regex_sb.append("(?:\\S+(?::\\S*)?@)?");
// Begin host group.
regex_sb.append("(?:");
// IP address (from http://www.regular-expressions.info/examples.html).
regex_sb.append("(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|");
// Host name or domain.
regex_sb.append("(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)(?:(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*){1,}[a-z\\u00a1-\\uffff0-9]{1,}))?|");
// Just path. Used in case of 'file://' scheme.
regex_sb.append("/(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)");
// End host group.
regex_sb.append(")");
// Port number.
regex_sb.append("(?::\\d{1,5})?");
// Resource path with optional query string.
regex_sb.append("(?:/[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
// Fragment.
regex_sb.append("(?:#[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
// End second matching group.
regex_sb.append(")");
URL_MATCH_REGEX = Pattern.compile(
regex_sb.toString(),
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
return URL_MATCH_REGEX;
}
public static LinkedHashSet<CharSequence> extractUrls(String text) {
LinkedHashSet<CharSequence> urlSet = new LinkedHashSet<>();
Matcher matcher = getUrlMatchRegex().matcher(text);
while (matcher.find()) {
int matchStart = matcher.start(1);
int matchEnd = matcher.end();
String url = text.substring(matchStart, matchEnd);
urlSet.add(url);
}
return urlSet;
}
/**
* Open a url.
*
* @param context The context for operations.
* @param url The url to open.
*/
public static void openUrl(final Context context, final String url) {
if (context == null || url == null || url.isEmpty()) return;
Uri uri = Uri.parse(url);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
try {
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
// If no activity found to handle intent, show system chooser
openSystemAppChooser(context, intent, context.getString(com.termux.R.string.title_open_url_with));
} catch (Exception e) {
Log.e(TermuxConstants.LOG_TAG, "Failed to open url \"" + url + "\"", e);
}
}
public static void openSystemAppChooser(final Context context, final Intent intent, final String title) {
if (context == null) return;
final Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER);
chooserIntent.putExtra(Intent.EXTRA_INTENT, intent);
chooserIntent.putExtra(Intent.EXTRA_TITLE, title);
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
context.startActivity(chooserIntent);
} catch (Exception e) {
Log.e(TermuxConstants.LOG_TAG, "Failed to open system chooser for: " + chooserIntent, e);
}
}
/**
* Share text.
*
* @param context The context for operations.
* @param subject The subject for sharing.
* @param text The text to share.
* @param title The title for share menu.
*/
public static void shareText(final Context context, final String subject, final String text, @Nullable final String title) {
if (context == null || text == null) return;
final Intent shareTextIntent = new Intent(Intent.ACTION_SEND);
shareTextIntent.setType("text/plain");
shareTextIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
shareTextIntent.putExtra(Intent.EXTRA_TEXT, text);
openSystemAppChooser(context, shareTextIntent, (title == null) ? context.getString(com.termux.R.string.title_share_with) : title);
}
/** Wrapper for {@link #copyTextToClipboard(Context, String, String, String)} with `null` `clipDataLabel` and `toastString`. */
public static void copyTextToClipboard(Context context, final String text) {
copyTextToClipboard(context, null, text, null);
}
/** Wrapper for {@link #copyTextToClipboard(Context, String, String, String)} with `null` `clipDataLabel`. */
public static void copyTextToClipboard(Context context, final String text, final String toastString) {
copyTextToClipboard(context, null, text, toastString);
}
/**
* Copy the text to primary clip of the clipboard.
*
* @param context The context for operations.
* @param clipDataLabel The label to show to the user describing the copied text.
* @param text The text to copy.
* @param toastString If this is not {@code null} or empty, then a toast is shown if copying to
* clipboard is successful.
*/
public static void copyTextToClipboard(Context context, @Nullable final String clipDataLabel,
final String text, final String toastString) {
ClipboardManager clipboardManager = context.getSystemService(ClipboardManager.class);
clipboardManager.setPrimaryClip(ClipData.newPlainText(clipDataLabel, text));
if (toastString != null && !toastString.isEmpty()) {
TermuxMessageDialogUtils.showToast(context, toastString);
}
}
/**
* Wrapper for {@link #getTextFromClipboard(Context, boolean)} that returns primary text {@link String}
* if its set and not empty.
*/
@Nullable
public static String getTextStringFromClipboardIfSet(Context context, boolean coerceToText) {
CharSequence textCharSequence = getTextFromClipboard(context, coerceToText);
if (textCharSequence == null) return null;
String textString = textCharSequence.toString();
return !textString.isEmpty() ? textString : null;
}
/**
* Get the text from primary clip of the clipboard.
*
* @param context The context for operations.
* @param coerceToText Whether to call {@link ClipData.Item#coerceToText(Context)} to coerce
* non-text data to text.
* @return Returns the {@link CharSequence} of primary text. This will be `null` if failed to get it.
*/
@Nullable
public static CharSequence getTextFromClipboard(Context context, boolean coerceToText) {
if (context == null) return null;
ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
if (clipboardManager == null) return null;
ClipData clipData = clipboardManager.getPrimaryClip();
if (clipData == null) return null;
ClipData.Item clipItem = clipData.getItemAt(0);
if (clipItem == null) return null;
return coerceToText ? clipItem.coerceToText(context) : clipItem.getText();
}
}

View File

@ -1,169 +0,0 @@
package com.termux.app.activities;
import android.content.Context;
import android.os.Bundle;
import android.os.Environment;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import com.termux.R;
import com.termux.shared.activities.ReportActivity;
import com.termux.shared.file.FileUtils;
import com.termux.shared.models.ReportInfo;
import com.termux.app.models.UserAction;
import com.termux.shared.interact.ShareUtils;
import com.termux.shared.android.PackageUtils;
import com.termux.shared.termux.settings.preferences.TermuxAPIAppSharedPreferences;
import com.termux.shared.termux.settings.preferences.TermuxFloatAppSharedPreferences;
import com.termux.shared.termux.settings.preferences.TermuxTaskerAppSharedPreferences;
import com.termux.shared.termux.settings.preferences.TermuxWidgetAppSharedPreferences;
import com.termux.shared.android.AndroidUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxUtils;
import com.termux.shared.activity.media.AppCompatActivityUtils;
import com.termux.shared.theme.NightMode;
public class SettingsActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AppCompatActivityUtils.setNightMode(this, NightMode.getAppNightMode().getName(), true);
setContentView(R.layout.activity_settings);
if (savedInstanceState == null) {
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.settings, new RootPreferencesFragment())
.commit();
}
AppCompatActivityUtils.setToolbar(this, com.termux.shared.R.id.toolbar);
AppCompatActivityUtils.setShowBackButtonInActionBar(this, true);
}
@Override
public boolean onSupportNavigateUp() {
onBackPressed();
return true;
}
public static class RootPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
setPreferencesFromResource(R.xml.root_preferences, rootKey);
new Thread() {
@Override
public void run() {
configureTermuxAPIPreference(context);
configureTermuxFloatPreference(context);
configureTermuxTaskerPreference(context);
configureTermuxWidgetPreference(context);
configureAboutPreference(context);
configureDonatePreference(context);
}
}.start();
}
private void configureTermuxAPIPreference(@NonNull Context context) {
Preference termuxAPIPreference = findPreference("termux_api");
if (termuxAPIPreference != null) {
TermuxAPIAppSharedPreferences preferences = TermuxAPIAppSharedPreferences.build(context, false);
// If failed to get app preferences, then likely app is not installed, so do not show its preference
termuxAPIPreference.setVisible(preferences != null);
}
}
private void configureTermuxFloatPreference(@NonNull Context context) {
Preference termuxFloatPreference = findPreference("termux_float");
if (termuxFloatPreference != null) {
TermuxFloatAppSharedPreferences preferences = TermuxFloatAppSharedPreferences.build(context, false);
// If failed to get app preferences, then likely app is not installed, so do not show its preference
termuxFloatPreference.setVisible(preferences != null);
}
}
private void configureTermuxTaskerPreference(@NonNull Context context) {
Preference termuxTaskerPreference = findPreference("termux_tasker");
if (termuxTaskerPreference != null) {
TermuxTaskerAppSharedPreferences preferences = TermuxTaskerAppSharedPreferences.build(context, false);
// If failed to get app preferences, then likely app is not installed, so do not show its preference
termuxTaskerPreference.setVisible(preferences != null);
}
}
private void configureTermuxWidgetPreference(@NonNull Context context) {
Preference termuxWidgetPreference = findPreference("termux_widget");
if (termuxWidgetPreference != null) {
TermuxWidgetAppSharedPreferences preferences = TermuxWidgetAppSharedPreferences.build(context, false);
// If failed to get app preferences, then likely app is not installed, so do not show its preference
termuxWidgetPreference.setVisible(preferences != null);
}
}
private void configureAboutPreference(@NonNull Context context) {
Preference aboutPreference = findPreference("about");
if (aboutPreference != null) {
aboutPreference.setOnPreferenceClickListener(preference -> {
new Thread() {
@Override
public void run() {
String title = "About";
StringBuilder aboutString = new StringBuilder();
aboutString.append(TermuxUtils.getAppInfoMarkdownString(context, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGES));
aboutString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context, true));
aboutString.append("\n\n").append(TermuxUtils.getImportantLinksMarkdownString(context));
String userActionName = UserAction.ABOUT.getName();
ReportInfo reportInfo = new ReportInfo(userActionName,
TermuxConstants.TERMUX_APP.TERMUX_SETTINGS_ACTIVITY_NAME, title);
reportInfo.setReportString(aboutString.toString());
reportInfo.setReportSaveFileLabelAndPath(userActionName,
Environment.getExternalStorageDirectory() + "/" +
FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true));
ReportActivity.startReportActivity(context, reportInfo);
}
}.start();
return true;
});
}
}
private void configureDonatePreference(@NonNull Context context) {
Preference donatePreference = findPreference("donate");
if (donatePreference != null) {
String signingCertificateSHA256Digest = PackageUtils.getSigningCertificateSHA256DigestForPackage(context);
if (signingCertificateSHA256Digest != null) {
// If APK is a Google Playstore release, then do not show the donation link
// since Termux isn't exempted from the playstore policy donation links restriction
// Check Fund solicitations: https://pay.google.com/intl/en_in/about/policy/
String apkRelease = TermuxUtils.getAPKRelease(signingCertificateSHA256Digest);
if (apkRelease == null || apkRelease.equals(TermuxConstants.APK_RELEASE_GOOGLE_PLAYSTORE_SIGNING_CERTIFICATE_SHA256_DIGEST)) {
donatePreference.setVisible(false);
return;
} else {
donatePreference.setVisible(true);
}
}
donatePreference.setOnPreferenceClickListener(preference -> {
ShareUtils.openUrl(context, TermuxConstants.TERMUX_DONATE_URL);
return true;
});
}
}
}
}

View File

@ -1,91 +0,0 @@
package com.termux.app.event;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.termux.shared.data.IntentUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.termux.TermuxUtils;
import com.termux.shared.termux.file.TermuxFileUtils;
import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment;
import com.termux.shared.termux.shell.TermuxShellManager;
public class SystemEventReceiver extends BroadcastReceiver {
private static SystemEventReceiver mInstance;
private static final String LOG_TAG = "SystemEventReceiver";
public static synchronized SystemEventReceiver getInstance() {
if (mInstance == null) {
mInstance = new SystemEventReceiver();
}
return mInstance;
}
@Override
public void onReceive(@NonNull Context context, @Nullable Intent intent) {
if (intent == null) return;
Logger.logDebug(LOG_TAG, "Intent Received:\n" + IntentUtils.getIntentString(intent));
String action = intent.getAction();
if (action == null) return;
switch (action) {
case Intent.ACTION_BOOT_COMPLETED:
onActionBootCompleted(context, intent);
break;
case Intent.ACTION_PACKAGE_ADDED:
case Intent.ACTION_PACKAGE_REMOVED:
case Intent.ACTION_PACKAGE_REPLACED:
onActionPackageUpdated(context, intent);
break;
default:
Logger.logError(LOG_TAG, "Invalid action \"" + action + "\" passed to " + LOG_TAG);
}
}
public synchronized void onActionBootCompleted(@NonNull Context context, @NonNull Intent intent) {
TermuxShellManager.onActionBootCompleted(context, intent);
}
public synchronized void onActionPackageUpdated(@NonNull Context context, @NonNull Intent intent) {
Uri data = intent.getData();
if (data != null && TermuxUtils.isUriDataForTermuxPluginPackage(data)) {
Logger.logDebug(LOG_TAG, intent.getAction().replaceAll("^android.intent.action.", "") +
" event received for \"" + data.toString().replaceAll("^package:", "") + "\"");
if (TermuxFileUtils.isTermuxFilesDirectoryAccessible(context, false, false) == null)
TermuxShellEnvironment.writeEnvironmentToFile(context);
}
}
/**
* Register {@link SystemEventReceiver} to listen to {@link Intent#ACTION_PACKAGE_ADDED},
* {@link Intent#ACTION_PACKAGE_REMOVED} and {@link Intent#ACTION_PACKAGE_REPLACED} broadcasts.
* They must be registered dynamically and cannot be registered implicitly in
* the AndroidManifest.xml due to Android 8+ restrictions.
*
* https://developer.android.com/guide/components/broadcast-exceptions
*/
public synchronized static void registerPackageUpdateEvents(@NonNull Context context) {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
intentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED);
intentFilter.addDataScheme("package");
context.registerReceiver(getInstance(), intentFilter);
}
public synchronized static void unregisterPackageUpdateEvents(@NonNull Context context) {
context.unregisterReceiver(getInstance());
}
}

View File

@ -1,4 +1,4 @@
package com.termux.shared.termux.extrakeys;
package com.termux.app.extrakeys;
import android.text.TextUtils;

View File

@ -1,4 +1,4 @@
package com.termux.shared.termux.extrakeys;
package com.termux.app.extrakeys;
import android.view.KeyEvent;
@ -13,8 +13,8 @@ public class ExtraKeysConstants {
public static List<String> PRIMARY_REPETITIVE_KEYS = Arrays.asList(
"UP", "DOWN", "LEFT", "RIGHT",
"BKSP", "DEL",
"PGUP", "PGDN");
"PGUP", "PGDN"
);
/** Defines the {@link KeyEvent} for common keys. */

View File

@ -1,13 +1,10 @@
package com.termux.shared.termux.extrakeys;
package com.termux.app.extrakeys;
import android.view.View;
import android.widget.Button;
import androidx.annotation.NonNull;
import com.google.android.material.button.MaterialButton;
import com.termux.shared.termux.extrakeys.ExtraKeysConstants.EXTRA_KEY_DISPLAY_MAPS;
import com.termux.shared.termux.terminal.io.TerminalExtraKeys;
import org.json.JSONArray;
import org.json.JSONException;
@ -93,38 +90,12 @@ public class ExtraKeysInfo {
*/
private final ExtraKeyButton[][] mButtons;
/**
* Initialize {@link ExtraKeysInfo}.
*
* @param propertiesInfo The {@link String} containing the info to create the {@link ExtraKeysInfo}.
* Check the class javadoc for details.
* @param style The style to pass to {@link #getCharDisplayMapForStyle(String)} to get the
* {@link ExtraKeysConstants.ExtraKeyDisplayMap} that defines the display text
* mapping for the keys if a custom value is not defined by
* {@link ExtraKeyButton#KEY_DISPLAY_NAME} for a key.
* @param extraKeyAliasMap The {@link ExtraKeysConstants.ExtraKeyDisplayMap} that defines the
* aliases for the actual key names. You can create your own or
* optionally pass {@link ExtraKeysConstants#CONTROL_CHARS_ALIASES}.
*/
public ExtraKeysInfo(@NonNull String propertiesInfo, String style,
public ExtraKeysInfo(@NonNull String propertiesInfo,
String style,
@NonNull ExtraKeysConstants.ExtraKeyDisplayMap extraKeyAliasMap) throws JSONException {
mButtons = initExtraKeysInfo(propertiesInfo, getCharDisplayMapForStyle(style), extraKeyAliasMap);
}
/**
* Initialize {@link ExtraKeysInfo}.
*
* @param propertiesInfo The {@link String} containing the info to create the {@link ExtraKeysInfo}.
* Check the class javadoc for details.
* @param extraKeyDisplayMap The {@link ExtraKeysConstants.ExtraKeyDisplayMap} that defines the
* display text mapping for the keys if a custom value is not defined
* by {@link ExtraKeyButton#KEY_DISPLAY_NAME} for a key. You can create
* your own or optionally pass one of the values defined in
* {@link #getCharDisplayMapForStyle(String)}.
* @param extraKeyAliasMap The {@link ExtraKeysConstants.ExtraKeyDisplayMap} that defines the
* aliases for the actual key names. You can create your own or
* optionally pass {@link ExtraKeysConstants#CONTROL_CHARS_ALIASES}.
*/
public ExtraKeysInfo(@NonNull String propertiesInfo,
@NonNull ExtraKeysConstants.ExtraKeyDisplayMap extraKeyDisplayMap,
@NonNull ExtraKeysConstants.ExtraKeyDisplayMap extraKeyAliasMap) throws JSONException {
@ -198,15 +169,15 @@ public class ExtraKeysInfo {
public static ExtraKeysConstants.ExtraKeyDisplayMap getCharDisplayMapForStyle(String style) {
switch (style) {
case "arrows-only":
return EXTRA_KEY_DISPLAY_MAPS.ARROWS_ONLY_CHAR_DISPLAY;
return ExtraKeysConstants.EXTRA_KEY_DISPLAY_MAPS.ARROWS_ONLY_CHAR_DISPLAY;
case "arrows-all":
return EXTRA_KEY_DISPLAY_MAPS.LOTS_OF_ARROWS_CHAR_DISPLAY;
return ExtraKeysConstants.EXTRA_KEY_DISPLAY_MAPS.LOTS_OF_ARROWS_CHAR_DISPLAY;
case "all":
return EXTRA_KEY_DISPLAY_MAPS.FULL_ISO_CHAR_DISPLAY;
return ExtraKeysConstants.EXTRA_KEY_DISPLAY_MAPS.FULL_ISO_CHAR_DISPLAY;
case "none":
return new ExtraKeysConstants.ExtraKeyDisplayMap();
default:
return EXTRA_KEY_DISPLAY_MAPS.DEFAULT_CHAR_DISPLAY;
return ExtraKeysConstants.EXTRA_KEY_DISPLAY_MAPS.DEFAULT_CHAR_DISPLAY;
}
}

View File

@ -1,4 +1,4 @@
package com.termux.shared.termux.extrakeys;
package com.termux.app.extrakeys;
import android.annotation.SuppressLint;
import android.content.Context;
@ -7,24 +7,11 @@ import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.util.AttributeSet;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ScheduledExecutorService;
import java.util.Map;
import java.util.HashMap;
import java.util.stream.Collectors;
import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.GridLayout;
import android.widget.PopupWindow;
@ -32,50 +19,22 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.button.MaterialButton;
import com.termux.shared.R;
import com.termux.shared.termux.terminal.io.TerminalExtraKeys;
import com.termux.shared.theme.ThemeUtils;
/**
* A {@link View} showing extra keys (such as Escape, Ctrl, Alt) not normally available on an Android soft
* keyboards.
*
* To use it, add following to a layout file and import it in your activity layout file or inflate
* it with a {@link androidx.viewpager.widget.ViewPager}.:
* {@code
* <?xml version="1.0" encoding="utf-8"?>
* <com.termux.shared.termux.extrakeys.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
* android:id="@+id/extra_keys"
* style="?android:attr/buttonBarStyle"
* android:layout_width="match_parent"
* android:layout_height="match_parent"
* android:layout_alignParentBottom="true"
* android:orientation="horizontal" />
* }
*
* Then in your activity, get its reference by a call to {@link android.app.Activity#findViewById(int)}
* or {@link LayoutInflater#inflate(int, ViewGroup)} if using {@link androidx.viewpager.widget.ViewPager}.
* Then call {@link #setExtraKeysViewClient(IExtraKeysView)} and pass it the implementation of
* {@link IExtraKeysView} so that you can receive callbacks. You can also override other values set
* in {@link ExtraKeysView#ExtraKeysView(Context, AttributeSet)} by calling the respective functions.
* If you extend {@link ExtraKeysView}, you can also set them in the constructor, but do call super().
*
* After this you will have to make a call to {@link ExtraKeysView#reload(ExtraKeysInfo, float) and pass
* it the {@link ExtraKeysInfo} to load and display the extra keys. Read its class javadocs for more
* info on how to create it.
*
* Termux app defines the view in res/layout/view_terminal_toolbar_extra_keys and
* inflates it in TerminalToolbarViewPager.instantiateItem() and sets the {@link ExtraKeysView} client
* and calls {@link ExtraKeysView#reload(ExtraKeysInfo).
* The {@link ExtraKeysInfo} is created by TermuxAppSharedProperties.setExtraKeys().
* Then its got and the view height is adjusted in TermuxActivity.setTerminalToolbarHeight().
* The client used is TermuxTerminalExtraKeys, which extends
* {@link TerminalExtraKeys } to handle Termux app specific logic and
* leave the rest to the super class.
*/
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
public final class ExtraKeysView extends GridLayout {
/** The client for the {@link ExtraKeysView}. */
/**
* The client for the {@link ExtraKeysView}.
*/
public interface IExtraKeysView {
/**
@ -84,22 +43,22 @@ public final class ExtraKeysView extends GridLayout {
* However, this is not called for {@link #mSpecialButtons}, whose state can instead be read
* via a call to {@link #readSpecialButton(SpecialButton, boolean)}.
*
* @param view The view that was clicked.
* @param view The view that was clicked.
* @param buttonInfo The {@link ExtraKeyButton} for the button that was clicked.
* The button may be a {@link ExtraKeyButton#KEY_MACRO} set which can be
* checked with a call to {@link ExtraKeyButton#isMacro()}.
* @param button The {@link MaterialButton} that was clicked.
* @param button The {@link MaterialButton} that was clicked.
*/
void onExtraKeyButtonClick(View view, ExtraKeyButton buttonInfo, MaterialButton button);
/**
* This is called by {@link ExtraKeysView} when a button is clicked so that the client
* can perform any hepatic feedback. This is only called in the {@link MaterialButton.OnClickListener}
* can perform any hepatic feedback. This is only called in the {@link OnClickListener}
* and not for every repeat. Its also called for {@link #mSpecialButtons}.
*
* @param view The view that was clicked.
* @param view The view that was clicked.
* @param buttonInfo The {@link ExtraKeyButton} for the button that was clicked.
* @param button The {@link MaterialButton} that was clicked.
* @param button The {@link MaterialButton} that was clicked.
* @return Return {@code true} if the client handled the feedback, otherwise {@code false}
* so that {@link ExtraKeysView#performExtraKeyButtonHapticFeedback(View, ExtraKeyButton, MaterialButton)}
* can handle it depending on system settings.
@ -108,52 +67,47 @@ public final class ExtraKeysView extends GridLayout {
}
/** Defines the default value for {@link #mButtonTextColor} defined by current theme. */
public static final int ATTR_BUTTON_TEXT_COLOR = R.attr.extraKeysButtonTextColor;
/** Defines the default value for {@link #mButtonActiveTextColor} defined by current theme. */
public static final int ATTR_BUTTON_ACTIVE_TEXT_COLOR = R.attr.extraKeysButtonActiveTextColor;
/** Defines the default value for {@link #mButtonBackgroundColor} defined by current theme. */
public static final int ATTR_BUTTON_BACKGROUND_COLOR = R.attr.extraKeysButtonBackgroundColor;
/** Defines the default value for {@link #mButtonActiveBackgroundColor} defined by current theme. */
public static final int ATTR_BUTTON_ACTIVE_BACKGROUND_COLOR = R.attr.extraKeysButtonActiveBackgroundColor;
/** Defines the default fallback value for {@link #mButtonTextColor} if {@link #ATTR_BUTTON_TEXT_COLOR} is undefined. */
public static final int DEFAULT_BUTTON_TEXT_COLOR = 0xFFFFFFFF;
/** Defines the default fallback value for {@link #mButtonActiveTextColor} if {@link #ATTR_BUTTON_ACTIVE_TEXT_COLOR} is undefined. */
public static final int DEFAULT_BUTTON_ACTIVE_TEXT_COLOR = 0xFF80DEEA;
/** Defines the default fallback value for {@link #mButtonBackgroundColor} if {@link #ATTR_BUTTON_BACKGROUND_COLOR} is undefined. */
public static final int DEFAULT_BUTTON_BACKGROUND_COLOR = 0x00000000;
/** Defines the default fallback value for {@link #mButtonActiveBackgroundColor} if {@link #ATTR_BUTTON_ACTIVE_BACKGROUND_COLOR} is undefined. */
public static final int DEFAULT_BUTTON_ACTIVE_BACKGROUND_COLOR = 0xFF7F7F7F;
/** Defines the minimum allowed duration in milliseconds for {@link #mLongPressTimeout}. */
/**
* Defines the minimum allowed duration in milliseconds for {@link #mLongPressTimeout}.
*/
public static final int MIN_LONG_PRESS_DURATION = 200;
/** Defines the maximum allowed duration in milliseconds for {@link #mLongPressTimeout}. */
/**
* Defines the maximum allowed duration in milliseconds for {@link #mLongPressTimeout}.
*/
public static final int MAX_LONG_PRESS_DURATION = 3000;
/** Defines the fallback duration in milliseconds for {@link #mLongPressTimeout}. */
/**
* Defines the fallback duration in milliseconds for {@link #mLongPressTimeout}.
*/
public static final int FALLBACK_LONG_PRESS_DURATION = 400;
/** Defines the minimum allowed duration in milliseconds for {@link #mLongPressRepeatDelay}. */
/**
* Defines the minimum allowed duration in milliseconds for {@link #mLongPressRepeatDelay}.
*/
public static final int MIN_LONG_PRESS__REPEAT_DELAY = 5;
/** Defines the maximum allowed duration in milliseconds for {@link #mLongPressRepeatDelay}. */
/**
* Defines the maximum allowed duration in milliseconds for {@link #mLongPressRepeatDelay}.
*/
public static final int MAX_LONG_PRESS__REPEAT_DELAY = 2000;
/** Defines the default duration in milliseconds for {@link #mLongPressRepeatDelay}. */
/**
* Defines the default duration in milliseconds for {@link #mLongPressRepeatDelay}.
*/
public static final int DEFAULT_LONG_PRESS_REPEAT_DELAY = 80;
/** The implementation of the {@link IExtraKeysView} that acts as a client for the {@link ExtraKeysView}. */
/**
* The implementation of the {@link IExtraKeysView} that acts as a client for the {@link ExtraKeysView}.
*/
protected IExtraKeysView mExtraKeysViewClient;
/** The map for the {@link SpecialButton} and their {@link SpecialButtonState}. Defaults to
* the one returned by {@link #getDefaultSpecialButtons(ExtraKeysView)}. */
/**
* The map for the {@link SpecialButton} and their {@link SpecialButtonState}. Defaults to
* the one returned by {@link #getDefaultSpecialButtons(ExtraKeysView)}.
*/
protected Map<SpecialButton, SpecialButtonState> mSpecialButtons;
/** The keys for the {@link SpecialButton} added to {@link #mSpecialButtons}. This is automatically
* set when the call to {@link #setSpecialButtons(Map)} is made. */
/**
* The keys for the {@link SpecialButton} added to {@link #mSpecialButtons}. This is automatically
* set when the call to {@link #setSpecialButtons(Map)} is made.
*/
protected Set<String> mSpecialButtonsKeys;
@ -166,18 +120,28 @@ public final class ExtraKeysView extends GridLayout {
protected List<String> mRepetitiveKeys;
/** The text color for the extra keys button. Defaults to {@link #DEFAULT_BUTTON_TEXT_COLOR}. */
/**
* The text color for the extra keys button. Defaults to {@link #DEFAULT_BUTTON_TEXT_COLOR}.
*/
protected int mButtonTextColor;
/** The text color for the extra keys button when its active.
* Defaults to {@link #DEFAULT_BUTTON_ACTIVE_TEXT_COLOR}. */
/**
* The text color for the extra keys button when its active.
* Defaults to {@link #DEFAULT_BUTTON_ACTIVE_TEXT_COLOR}.
*/
protected int mButtonActiveTextColor;
/** The background color for the extra keys button. Defaults to {@link #DEFAULT_BUTTON_BACKGROUND_COLOR}. */
/**
* The background color for the extra keys button. Defaults to {@link #DEFAULT_BUTTON_BACKGROUND_COLOR}.
*/
protected int mButtonBackgroundColor;
/** The background color for the extra keys button when its active. Defaults to
* {@link #DEFAULT_BUTTON_ACTIVE_BACKGROUND_COLOR}. */
/**
* The background color for the extra keys button when its active. Defaults to
* {@link #DEFAULT_BUTTON_ACTIVE_BACKGROUND_COLOR}.
*/
protected int mButtonActiveBackgroundColor;
/** Defines whether text for the extra keys button should be all capitalized automatically. */
/**
* Defines whether text for the extra keys button should be all capitalized automatically.
*/
protected boolean mButtonTextAllCaps = true;
@ -199,8 +163,10 @@ public final class ExtraKeysView extends GridLayout {
protected int mLongPressRepeatDelay;
/** The popup window shown if {@link ExtraKeyButton#getPopup()} returns a {@code non-null} value
* and a swipe up action is done on an extra key. */
/**
* The popup window shown if {@link ExtraKeyButton#getPopup()} returns a {@code non-null} value
* and a swipe up action is done on an extra key.
*/
protected PopupWindow mPopupWindow;
protected ScheduledExecutorService mScheduledExecutor;
@ -215,130 +181,143 @@ public final class ExtraKeysView extends GridLayout {
setRepetitiveKeys(ExtraKeysConstants.PRIMARY_REPETITIVE_KEYS);
setSpecialButtons(getDefaultSpecialButtons(this));
setButtonColors(
ThemeUtils.getSystemAttrColor(context, ATTR_BUTTON_TEXT_COLOR, DEFAULT_BUTTON_TEXT_COLOR),
ThemeUtils.getSystemAttrColor(context, ATTR_BUTTON_ACTIVE_TEXT_COLOR, DEFAULT_BUTTON_ACTIVE_TEXT_COLOR),
ThemeUtils.getSystemAttrColor(context, ATTR_BUTTON_BACKGROUND_COLOR, DEFAULT_BUTTON_BACKGROUND_COLOR),
ThemeUtils.getSystemAttrColor(context, ATTR_BUTTON_ACTIVE_BACKGROUND_COLOR, DEFAULT_BUTTON_ACTIVE_BACKGROUND_COLOR));
setLongPressTimeout(ViewConfiguration.getLongPressTimeout());
setLongPressRepeatDelay(DEFAULT_LONG_PRESS_REPEAT_DELAY);
}
/** Get {@link #mExtraKeysViewClient}. */
/**
* Get {@link #mExtraKeysViewClient}.
*/
public IExtraKeysView getExtraKeysViewClient() {
return mExtraKeysViewClient;
}
/** Set {@link #mExtraKeysViewClient}. */
/**
* Set {@link #mExtraKeysViewClient}.
*/
public void setExtraKeysViewClient(IExtraKeysView extraKeysViewClient) {
mExtraKeysViewClient = extraKeysViewClient;
}
/** Get {@link #mRepetitiveKeys}. */
/**
* Get {@link #mRepetitiveKeys}.
*/
public List<String> getRepetitiveKeys() {
if (mRepetitiveKeys == null) return null;
return mRepetitiveKeys.stream().map(String::new).collect(Collectors.toList());
}
/** Set {@link #mRepetitiveKeys}. Must not be {@code null}. */
/**
* Set {@link #mRepetitiveKeys}. Must not be {@code null}.
*/
public void setRepetitiveKeys(@NonNull List<String> repetitiveKeys) {
mRepetitiveKeys = repetitiveKeys;
}
/** Get {@link #mSpecialButtons}. */
/**
* Get {@link #mSpecialButtons}.
*/
public Map<SpecialButton, SpecialButtonState> getSpecialButtons() {
if (mSpecialButtons == null) return null;
return mSpecialButtons.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
/** Get {@link #mSpecialButtonsKeys}. */
/**
* Get {@link #mSpecialButtonsKeys}.
*/
public Set<String> getSpecialButtonsKeys() {
if (mSpecialButtonsKeys == null) return null;
return mSpecialButtonsKeys.stream().map(String::new).collect(Collectors.toSet());
}
/** Set {@link #mSpecialButtonsKeys}. Must not be {@code null}. */
/**
* Set {@link #mSpecialButtonsKeys}. Must not be {@code null}.
*/
public void setSpecialButtons(@NonNull Map<SpecialButton, SpecialButtonState> specialButtons) {
mSpecialButtons = specialButtons;
mSpecialButtonsKeys = this.mSpecialButtons.keySet().stream().map(SpecialButton::getKey).collect(Collectors.toSet());
}
/**
* Set the {@link ExtraKeysView} button colors.
*
* @param buttonTextColor The value for {@link #mButtonTextColor}.
* @param buttonActiveTextColor The value for {@link #mButtonActiveTextColor}.
* @param buttonBackgroundColor The value for {@link #mButtonBackgroundColor}.
* @param buttonActiveBackgroundColor The value for {@link #mButtonActiveBackgroundColor}.
* Get {@link #mButtonTextColor}.
*/
public void setButtonColors(int buttonTextColor, int buttonActiveTextColor, int buttonBackgroundColor, int buttonActiveBackgroundColor) {
mButtonTextColor = buttonTextColor;
mButtonActiveTextColor = buttonActiveTextColor;
mButtonBackgroundColor = buttonBackgroundColor;
mButtonActiveBackgroundColor = buttonActiveBackgroundColor;
}
/** Get {@link #mButtonTextColor}. */
public int getButtonTextColor() {
return mButtonTextColor;
}
/** Set {@link #mButtonTextColor}. */
/**
* Set {@link #mButtonTextColor}.
*/
public void setButtonTextColor(int buttonTextColor) {
mButtonTextColor = buttonTextColor;
}
/** Get {@link #mButtonActiveTextColor}. */
/**
* Get {@link #mButtonActiveTextColor}.
*/
public int getButtonActiveTextColor() {
return mButtonActiveTextColor;
}
/** Set {@link #mButtonActiveTextColor}. */
/**
* Set {@link #mButtonActiveTextColor}.
*/
public void setButtonActiveTextColor(int buttonActiveTextColor) {
mButtonActiveTextColor = buttonActiveTextColor;
}
/** Get {@link #mButtonBackgroundColor}. */
/**
* Get {@link #mButtonBackgroundColor}.
*/
public int getButtonBackgroundColor() {
return mButtonBackgroundColor;
}
/** Set {@link #mButtonBackgroundColor}. */
/**
* Set {@link #mButtonBackgroundColor}.
*/
public void setButtonBackgroundColor(int buttonBackgroundColor) {
mButtonBackgroundColor = buttonBackgroundColor;
}
/** Get {@link #mButtonActiveBackgroundColor}. */
/**
* Get {@link #mButtonActiveBackgroundColor}.
*/
public int getButtonActiveBackgroundColor() {
return mButtonActiveBackgroundColor;
}
/** Set {@link #mButtonActiveBackgroundColor}. */
/**
* Set {@link #mButtonActiveBackgroundColor}.
*/
public void setButtonActiveBackgroundColor(int buttonActiveBackgroundColor) {
mButtonActiveBackgroundColor = buttonActiveBackgroundColor;
}
/** Set {@link #mButtonTextAllCaps}. */
/**
* Set {@link #mButtonTextAllCaps}.
*/
public void setButtonTextAllCaps(boolean buttonTextAllCaps) {
mButtonTextAllCaps = buttonTextAllCaps;
}
/** Get {@link #mLongPressTimeout}. */
/**
* Get {@link #mLongPressTimeout}.
*/
public int getLongPressTimeout() {
return mLongPressTimeout;
}
/** Set {@link #mLongPressTimeout}. */
/**
* Set {@link #mLongPressTimeout}.
*/
public void setLongPressTimeout(int longPressDuration) {
if (longPressDuration >= MIN_LONG_PRESS_DURATION && longPressDuration <= MAX_LONG_PRESS_DURATION) {
mLongPressTimeout = longPressDuration;
@ -347,12 +326,16 @@ public final class ExtraKeysView extends GridLayout {
}
}
/** Get {@link #mLongPressRepeatDelay}. */
/**
* Get {@link #mLongPressRepeatDelay}.
*/
public int getLongPressRepeatDelay() {
return mLongPressRepeatDelay;
}
/** Set {@link #mLongPressRepeatDelay}. */
/**
* Set {@link #mLongPressRepeatDelay}.
*/
public void setLongPressRepeatDelay(int longPressRepeatDelay) {
if (mLongPressRepeatDelay >= MIN_LONG_PRESS__REPEAT_DELAY && mLongPressRepeatDelay <= MAX_LONG_PRESS__REPEAT_DELAY) {
mLongPressRepeatDelay = longPressRepeatDelay;
@ -362,7 +345,9 @@ public final class ExtraKeysView extends GridLayout {
}
/** Get the default map that can be used for {@link #mSpecialButtons}. */
/**
* Get the default map that can be used for {@link #mSpecialButtons}.
*/
@NonNull
public Map<SpecialButton, SpecialButtonState> getDefaultSpecialButtons(ExtraKeysView extraKeysView) {
return new HashMap<SpecialButton, SpecialButtonState>() {{
@ -374,20 +359,19 @@ public final class ExtraKeysView extends GridLayout {
}
/**
* Reload this instance of {@link ExtraKeysView} with the info passed in {@code extraKeysInfo}.
*
* @param extraKeysInfo The {@link ExtraKeysInfo} that defines the necessary info for the extra keys.
* @param heightPx The height in pixels of the parent surrounding the {@link ExtraKeysView}. It must
* be a single child.
* @param heightPx The height in pixels of the parent surrounding the {@link ExtraKeysView}. It must
* be a single child.
*/
@SuppressLint("ClickableViewAccessibility")
public void reload(ExtraKeysInfo extraKeysInfo, float heightPx) {
if (extraKeysInfo == null)
return;
for(SpecialButtonState state : mSpecialButtons.values())
for (SpecialButtonState state : mSpecialButtons.values())
state.buttons = new ArrayList<>();
removeAllViews();
@ -469,13 +453,9 @@ public final class ExtraKeysView extends GridLayout {
}
});
LayoutParams param = new GridLayout.LayoutParams();
LayoutParams param = new LayoutParams();
param.width = 0;
if(Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) {
param.height = (int)(heightPx + 0.5);
} else {
param.height = 0;
}
param.height = 0;
param.setMargins(0, 0, 0, 0);
param.columnSpec = GridLayout.spec(col, GridLayout.FILL, 1.f);
param.rowSpec = GridLayout.spec(row, GridLayout.FILL, 1.f);
@ -487,7 +467,6 @@ public final class ExtraKeysView extends GridLayout {
}
public void onExtraKeyButtonClick(View view, ExtraKeyButton buttonInfo, MaterialButton button) {
if (mExtraKeysViewClient != null)
mExtraKeysViewClient.onExtraKeyButtonClick(view, buttonInfo, button);
@ -515,7 +494,6 @@ public final class ExtraKeysView extends GridLayout {
}
public void onAnyExtraKeyButtonClick(View view, @NonNull ExtraKeyButton buttonInfo, MaterialButton button) {
if (isSpecialButton(buttonInfo)) {
if (mLongPressCount > 0) return;
@ -585,7 +563,6 @@ public final class ExtraKeysView extends GridLayout {
}
void showPopup(View view, ExtraKeyButton extraButton) {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
@ -623,8 +600,9 @@ public final class ExtraKeysView extends GridLayout {
}
/** Check whether a {@link ExtraKeyButton} is a {@link SpecialButton}. */
/**
* Check whether a {@link ExtraKeyButton} is a {@link SpecialButton}.
*/
public boolean isSpecialButton(ExtraKeyButton button) {
return mSpecialButtonsKeys.contains(button.getKey());
}
@ -632,12 +610,12 @@ public final class ExtraKeysView extends GridLayout {
/**
* Read whether {@link SpecialButton} registered in {@link #mSpecialButtons} is active or not.
*
* @param specialButton The {@link SpecialButton} to read.
* @param specialButton The {@link SpecialButton} to read.
* @param autoSetInActive Set to {@code true} if {@link SpecialButtonState#isActive} should be
* set {@code false} if button is not locked.
* @return Returns {@code null} if button does not exist in {@link #mSpecialButtons}. If button
* exists, then returns {@code true} if the button is created in {@link ExtraKeysView}
* and is active, otherwise {@code false}.
* exists, then returns {@code true} if the button is created in {@link ExtraKeysView}
* and is active, otherwise {@code false}.
*/
@Nullable
public Boolean readSpecialButton(SpecialButton specialButton, boolean autoSetInActive) {
@ -667,7 +645,6 @@ public final class ExtraKeysView extends GridLayout {
}
/**
* General util function to compute the longest column length in a matrix.
*/

View File

@ -1,4 +1,4 @@
package com.termux.app.terminal.io;
package com.termux.app.extrakeys;
public class KeyboardShortcut {

View File

@ -1,4 +1,4 @@
package com.termux.shared.termux.extrakeys;
package com.termux.app.extrakeys;
import androidx.annotation.NonNull;

View File

@ -1,4 +1,4 @@
package com.termux.shared.termux.extrakeys;
package com.termux.app.extrakeys;
import com.google.android.material.button.MaterialButton;

View File

@ -1,4 +1,4 @@
package com.termux.app.terminal.io;
package com.termux.app.extrakeys;
import android.view.LayoutInflater;
import android.view.View;
@ -11,7 +11,6 @@ import androidx.viewpager.widget.ViewPager;
import com.termux.R;
import com.termux.app.TermuxActivity;
import com.termux.shared.termux.extrakeys.ExtraKeysView;
import com.termux.terminal.TerminalSession;
public class TerminalToolbarViewPager {
@ -45,16 +44,10 @@ public class TerminalToolbarViewPager {
layout = inflater.inflate(R.layout.view_terminal_toolbar_extra_keys, collection, false);
ExtraKeysView extraKeysView = (ExtraKeysView) layout;
extraKeysView.setExtraKeysViewClient(mActivity.getTermuxTerminalExtraKeys());
extraKeysView.setButtonTextAllCaps(mActivity.getProperties().shouldExtraKeysTextBeAllCaps());
extraKeysView.setButtonTextAllCaps(false);
mActivity.setExtraKeysView(extraKeysView);
extraKeysView.reload(mActivity.getTermuxTerminalExtraKeys().getExtraKeysInfo(),
mActivity.getTerminalToolbarDefaultHeight());
// apply extra keys fix if enabled in prefs
if (mActivity.getProperties().isUsingFullScreen() && mActivity.getProperties().isUsingFullScreenWorkAround()) {
FullScreenWorkAround.apply(mActivity);
}
} else {
layout = inflater.inflate(R.layout.view_terminal_toolbar_text_input, collection, false);
final EditText editText = layout.findViewById(R.id.terminal_toolbar_text_input);
@ -87,11 +80,8 @@ public class TerminalToolbarViewPager {
public void destroyItem(@NonNull ViewGroup collection, int position, @NonNull Object view) {
collection.removeView((View) view);
}
}
public static class OnPageChangeListener extends ViewPager.SimpleOnPageChangeListener {
final TermuxActivity mActivity;
@ -108,10 +98,11 @@ public class TerminalToolbarViewPager {
mActivity.getTerminalView().requestFocus();
} else {
final EditText editText = mTerminalToolbarViewPager.findViewById(R.id.terminal_toolbar_text_input);
if (editText != null) editText.requestFocus();
if (editText != null) {
editText.requestFocus();
}
}
}
}
}

View File

@ -0,0 +1,179 @@
package com.termux.app.extrakeys;
import android.annotation.SuppressLint;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.drawerlayout.widget.DrawerLayout;
import com.google.android.material.button.MaterialButton;
import com.termux.app.TermuxActivity;
import com.termux.app.TermuxTerminalSessionActivityClient;
import com.termux.app.TermuxTerminalViewClient;
import com.termux.view.TerminalView;
import org.json.JSONException;
import java.util.HashMap;
import java.util.Map;
public class TermuxTerminalExtraKeys implements ExtraKeysView.IExtraKeysView {
public static Map<String, Integer> PRIMARY_KEY_CODES_FOR_STRINGS = new HashMap<>() {{
put("SPACE", KeyEvent.KEYCODE_SPACE);
put("ESC", KeyEvent.KEYCODE_ESCAPE);
put("TAB", KeyEvent.KEYCODE_TAB);
put("HOME", KeyEvent.KEYCODE_MOVE_HOME);
put("END", KeyEvent.KEYCODE_MOVE_END);
put("PGUP", KeyEvent.KEYCODE_PAGE_UP);
put("PGDN", KeyEvent.KEYCODE_PAGE_DOWN);
put("INS", KeyEvent.KEYCODE_INSERT);
put("DEL", KeyEvent.KEYCODE_FORWARD_DEL);
put("BKSP", KeyEvent.KEYCODE_DEL);
put("UP", KeyEvent.KEYCODE_DPAD_UP);
put("LEFT", KeyEvent.KEYCODE_DPAD_LEFT);
put("RIGHT", KeyEvent.KEYCODE_DPAD_RIGHT);
put("DOWN", KeyEvent.KEYCODE_DPAD_DOWN);
put("ENTER", KeyEvent.KEYCODE_ENTER);
put("F1", KeyEvent.KEYCODE_F1);
put("F2", KeyEvent.KEYCODE_F2);
put("F3", KeyEvent.KEYCODE_F3);
put("F4", KeyEvent.KEYCODE_F4);
put("F5", KeyEvent.KEYCODE_F5);
put("F6", KeyEvent.KEYCODE_F6);
put("F7", KeyEvent.KEYCODE_F7);
put("F8", KeyEvent.KEYCODE_F8);
put("F9", KeyEvent.KEYCODE_F9);
put("F10", KeyEvent.KEYCODE_F10);
put("F11", KeyEvent.KEYCODE_F11);
put("F12", KeyEvent.KEYCODE_F12);
}};
private ExtraKeysInfo mExtraKeysInfo;
final TermuxActivity mActivity;
final TermuxTerminalViewClient mTermuxTerminalViewClient;
final TermuxTerminalSessionActivityClient mTermuxTerminalSessionActivityClient;
private static final String LOG_TAG = "TermuxTerminalExtraKeys";
public TermuxTerminalExtraKeys(TermuxActivity activity, @NonNull TerminalView terminalView,
TermuxTerminalViewClient termuxTerminalViewClient,
TermuxTerminalSessionActivityClient termuxTerminalSessionActivityClient) {
mActivity = activity;
mTermuxTerminalViewClient = termuxTerminalViewClient;
mTermuxTerminalSessionActivityClient = termuxTerminalSessionActivityClient;
setExtraKeys();
}
/**
* Set the terminal extra keys and style.
*/
private void setExtraKeys() {
mExtraKeysInfo = null;
try {
// The mMap stores the extra key and style string values while loading properties
// Check {@link #getExtraKeysInternalPropertyValueFromValue(String)} and
// {@link #getExtraKeysStyleInternalPropertyValueFromValue(String)}
String extrakeys = mActivity.mProperties.getExtraKeys();
String extraKeysStyle = mActivity.mProperties.getExtraKeysStyle();
ExtraKeysConstants.ExtraKeyDisplayMap extraKeyDisplayMap = ExtraKeysInfo.getCharDisplayMapForStyle(extraKeysStyle);
if (ExtraKeysConstants.EXTRA_KEY_DISPLAY_MAPS.DEFAULT_CHAR_DISPLAY.equals(extraKeyDisplayMap) && !"default".equals(extraKeysStyle)) {
mActivity.showToast("The style \"" + extraKeysStyle + "\" is invalid. Using default style instead.", true);
extraKeysStyle = "default";
}
mExtraKeysInfo = new ExtraKeysInfo(extrakeys, extraKeysStyle, ExtraKeysConstants.CONTROL_CHARS_ALIASES);
} catch (JSONException e) {
mActivity.showToast("Could not load and set the extra keys property from the properties file: " + e, true);
//try {
mExtraKeysInfo = null; // TODO: new ExtraKeysInfo(TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE, ExtraKeysConstants.CONTROL_CHARS_ALIASES);
//} catch (JSONException e2) {
// We should be able to handle our own defaults!
//throw new RuntimeException(e2);
//}
}
}
public ExtraKeysInfo getExtraKeysInfo() {
return mExtraKeysInfo;
}
@SuppressLint("RtlHardcoded")
public void onTerminalExtraKeyButtonClick(View view, String key, boolean ctrlDown, boolean altDown, boolean shiftDown, boolean fnDown) {
if ("KEYBOARD".equals(key)) {
if(mTermuxTerminalViewClient != null)
mTermuxTerminalViewClient.onToggleSoftKeyboardRequest();
} else if ("DRAWER".equals(key)) {
DrawerLayout drawerLayout = mTermuxTerminalViewClient.getActivity().getDrawer();
if (drawerLayout.isDrawerOpen(Gravity.LEFT))
drawerLayout.closeDrawer(Gravity.LEFT);
else
drawerLayout.openDrawer(Gravity.LEFT);
} else if ("PASTE".equals(key)) {
if(mTermuxTerminalSessionActivityClient != null)
mTermuxTerminalSessionActivityClient.onPasteTextFromClipboard(null);
} else if ("SCROLL".equals(key)) {
TerminalView terminalView = mTermuxTerminalViewClient.getActivity().getTerminalView();
if (terminalView != null && terminalView.mEmulator != null)
terminalView.mEmulator.toggleAutoScrollDisabled();
} else {
if (PRIMARY_KEY_CODES_FOR_STRINGS.containsKey(key)) {
Integer keyCode = PRIMARY_KEY_CODES_FOR_STRINGS.get(key);
if (keyCode == null) return;
int metaState = 0;
if (ctrlDown) metaState |= KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON;
if (altDown) metaState |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON;
if (shiftDown) metaState |= KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_LEFT_ON;
if (fnDown) metaState |= KeyEvent.META_FUNCTION_ON;
KeyEvent keyEvent = new KeyEvent(0, 0, KeyEvent.ACTION_UP, keyCode, 0, metaState);
mActivity.getTerminalView().onKeyDown(keyCode, keyEvent);
} else {
// not a control char
key.codePoints().forEach(codePoint -> {
mActivity.getTerminalView().inputCodePoint(TerminalView.KEY_EVENT_SOURCE_VIRTUAL_KEYBOARD, codePoint, ctrlDown, altDown);
});
}
}
}
@Override
public void onExtraKeyButtonClick(View view, ExtraKeyButton buttonInfo, MaterialButton button) {
if (buttonInfo.isMacro()) {
String[] keys = buttonInfo.getKey().split(" ");
boolean ctrlDown = false;
boolean altDown = false;
boolean shiftDown = false;
boolean fnDown = false;
for (String key : keys) {
if (SpecialButton.CTRL.getKey().equals(key)) {
ctrlDown = true;
} else if (SpecialButton.ALT.getKey().equals(key)) {
altDown = true;
} else if (SpecialButton.SHIFT.getKey().equals(key)) {
shiftDown = true;
} else if (SpecialButton.FN.getKey().equals(key)) {
fnDown = true;
} else {
onTerminalExtraKeyButtonClick(view, key, ctrlDown, altDown, shiftDown, fnDown);
ctrlDown = false; altDown = false; shiftDown = false; fnDown = false;
}
}
} else {
onTerminalExtraKeyButtonClick(view, buttonInfo.getKey(), false, false, false, false);
}
}
@Override
public boolean performExtraKeyButtonHapticFeedback(View view, ExtraKeyButton buttonInfo, MaterialButton button) {
return false;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,155 +0,0 @@
package com.termux.app.fragments.settings.termux;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.ListPreference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceDataStore;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import com.termux.R;
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
import com.termux.shared.logger.Logger;
@Keep
public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.termux_debugging_preferences, rootKey);
configureLoggingPreferences(context);
}
private void configureLoggingPreferences(@NonNull Context context) {
PreferenceCategory loggingCategory = findPreference("logging");
if (loggingCategory == null) return;
ListPreference logLevelListPreference = findPreference("log_level");
if (logLevelListPreference != null) {
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context, true);
if (preferences == null) return;
setLogLevelListPreferenceData(logLevelListPreference, context, preferences.getLogLevel());
loggingCategory.addPreference(logLevelListPreference);
}
}
public static ListPreference setLogLevelListPreferenceData(ListPreference logLevelListPreference, Context context, int logLevel) {
if (logLevelListPreference == null)
logLevelListPreference = new ListPreference(context);
CharSequence[] logLevels = Logger.getLogLevelsArray();
CharSequence[] logLevelLabels = Logger.getLogLevelLabelsArray(context, logLevels, true);
logLevelListPreference.setEntryValues(logLevels);
logLevelListPreference.setEntries(logLevelLabels);
logLevelListPreference.setValue(String.valueOf(logLevel));
logLevelListPreference.setDefaultValue(Logger.DEFAULT_LOG_LEVEL);
return logLevelListPreference;
}
}
class DebuggingPreferencesDataStore extends PreferenceDataStore {
private final Context mContext;
private final TermuxAppSharedPreferences mPreferences;
private static DebuggingPreferencesDataStore mInstance;
private DebuggingPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxAppSharedPreferences.build(context, true);
}
public static synchronized DebuggingPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new DebuggingPreferencesDataStore(context);
}
return mInstance;
}
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
if (mPreferences == null) return null;
if (key == null) return null;
switch (key) {
case "log_level":
return String.valueOf(mPreferences.getLogLevel());
default:
return null;
}
}
@Override
public void putString(String key, @Nullable String value) {
if (mPreferences == null) return;
if (key == null) return;
switch (key) {
case "log_level":
if (value != null) {
mPreferences.setLogLevel(mContext, Integer.parseInt(value));
}
break;
default:
break;
}
}
@Override
public void putBoolean(String key, boolean value) {
if (mPreferences == null) return;
if (key == null) return;
switch (key) {
case "terminal_view_key_logging_enabled":
mPreferences.setTerminalViewKeyLoggingEnabled(value);
break;
case "plugin_error_notifications_enabled":
mPreferences.setPluginErrorNotificationsEnabled(value);
break;
case "crash_report_notifications_enabled":
mPreferences.setCrashReportNotificationsEnabled(value);
break;
default:
break;
}
}
@Override
public boolean getBoolean(String key, boolean defValue) {
if (mPreferences == null) return false;
switch (key) {
case "terminal_view_key_logging_enabled":
return mPreferences.isTerminalViewKeyLoggingEnabled();
case "plugin_error_notifications_enabled":
return mPreferences.arePluginErrorNotificationsEnabled(false);
case "crash_report_notifications_enabled":
return mPreferences.areCrashReportNotificationsEnabled(false);
default:
return false;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,18 +0,0 @@
package com.termux.app.models;
public enum UserAction {
ABOUT("about"),
REPORT_ISSUE_FROM_TRANSCRIPT("report issue from transcript");
private final String name;
UserAction(final String name) {
this.name = name;
}
public String getName() {
return name;
}
}

View File

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

View File

@ -1,31 +0,0 @@
package com.termux.app.terminal;
import android.app.Service;
import androidx.annotation.NonNull;
import com.termux.app.TermuxService;
import com.termux.shared.termux.shell.command.runner.terminal.TermuxSession;
import com.termux.shared.termux.terminal.TermuxTerminalSessionClientBase;
import com.termux.terminal.TerminalSession;
import com.termux.terminal.TerminalSessionClient;
/** The {@link TerminalSessionClient} implementation that may require a {@link Service} for its interface methods. */
public class TermuxTerminalSessionServiceClient extends TermuxTerminalSessionClientBase {
private static final String LOG_TAG = "TermuxTerminalSessionServiceClient";
private final TermuxService mService;
public TermuxTerminalSessionServiceClient(TermuxService service) {
this.mService = service;
}
@Override
public void setTerminalShellPid(@NonNull TerminalSession terminalSession, int pid) {
TermuxSession termuxSession = mService.getTermuxSessionForTerminalSession(terminalSession);
if (termuxSession != null)
termuxSession.getExecutionCommand().mPid = pid;
}
}

View File

@ -1,802 +0,0 @@
package com.termux.app.terminal;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.media.AudioManager;
import android.os.Environment;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.Toast;
import com.termux.R;
import com.termux.app.TermuxActivity;
import com.termux.shared.file.FileUtils;
import com.termux.shared.interact.MessageDialogUtils;
import com.termux.shared.interact.ShareUtils;
import com.termux.shared.shell.ShellUtils;
import com.termux.shared.termux.TermuxBootstrap;
import com.termux.shared.termux.terminal.TermuxTerminalViewClientBase;
import com.termux.shared.termux.extrakeys.SpecialButton;
import com.termux.shared.android.AndroidUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.activities.ReportActivity;
import com.termux.shared.models.ReportInfo;
import com.termux.app.models.UserAction;
import com.termux.app.terminal.io.KeyboardShortcut;
import com.termux.shared.termux.settings.properties.TermuxPropertyConstants;
import com.termux.shared.data.DataUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.termux.TermuxUtils;
import com.termux.shared.termux.data.TermuxUrlUtils;
import com.termux.shared.view.KeyboardUtils;
import com.termux.shared.view.ViewUtils;
import com.termux.terminal.KeyHandler;
import com.termux.terminal.TerminalEmulator;
import com.termux.terminal.TerminalSession;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import androidx.drawerlayout.widget.DrawerLayout;
public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
final TermuxActivity mActivity;
final TermuxTerminalSessionActivityClient mTermuxTerminalSessionActivityClient;
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
private Runnable mShowSoftKeyboardRunnable;
private boolean mShowSoftKeyboardIgnoreOnce;
private boolean mShowSoftKeyboardWithDelayOnce;
private boolean mTerminalCursorBlinkerStateAlreadySet;
private List<KeyboardShortcut> mSessionShortcuts;
private static final String LOG_TAG = "TermuxTerminalViewClient";
public TermuxTerminalViewClient(TermuxActivity activity, TermuxTerminalSessionActivityClient termuxTerminalSessionActivityClient) {
this.mActivity = activity;
this.mTermuxTerminalSessionActivityClient = termuxTerminalSessionActivityClient;
}
public TermuxActivity getActivity() {
return mActivity;
}
/**
* Should be called when mActivity.onCreate() is called
*/
public void onCreate() {
onReloadProperties();
mActivity.getTerminalView().setTextSize(mActivity.getPreferences().getFontSize());
mActivity.getTerminalView().setKeepScreenOn(mActivity.getPreferences().shouldKeepScreenOn());
}
/**
* Should be called when mActivity.onStart() is called
*/
public void onStart() {
// Set {@link TerminalView#TERMINAL_VIEW_KEY_LOGGING_ENABLED} value
// Also required if user changed the preference from {@link TermuxSettings} activity and returns
boolean isTerminalViewKeyLoggingEnabled = mActivity.getPreferences().isTerminalViewKeyLoggingEnabled();
mActivity.getTerminalView().setIsTerminalViewKeyLoggingEnabled(isTerminalViewKeyLoggingEnabled);
// Piggyback on the terminal view key logging toggle for now, should add a separate toggle in future
mActivity.getTermuxActivityRootView().setIsRootViewLoggingEnabled(isTerminalViewKeyLoggingEnabled);
ViewUtils.setIsViewUtilsLoggingEnabled(isTerminalViewKeyLoggingEnabled);
}
/**
* Should be called when mActivity.onResume() is called
*/
public void onResume() {
// Show the soft keyboard if required
setSoftKeyboardState(true, mActivity.isActivityRecreated());
mTerminalCursorBlinkerStateAlreadySet = false;
if (mActivity.getTerminalView().mEmulator != null) {
// Start terminal cursor blinking if enabled
// If emulator is already set, then start blinker now, otherwise wait for onEmulatorSet()
// event to start it. This is needed since onEmulatorSet() may not be called after
// TermuxActivity is started after device display timeout with double tap and not power button.
setTerminalCursorBlinkerState(true);
mTerminalCursorBlinkerStateAlreadySet = true;
}
}
/**
* Should be called when mActivity.onStop() is called
*/
public void onStop() {
// Stop terminal cursor blinking if enabled
setTerminalCursorBlinkerState(false);
}
/**
* Should be called when mActivity.reloadProperties() is called
*/
public void onReloadProperties() {
setSessionShortcuts();
}
/**
* Should be called when mActivity.reloadActivityStyling() is called
*/
public void onReloadActivityStyling() {
// Show the soft keyboard if required
setSoftKeyboardState(false, true);
// Start terminal cursor blinking if enabled
setTerminalCursorBlinkerState(true);
}
/**
* Should be called when {@link com.termux.view.TerminalView#mEmulator} is set
*/
@Override
public void onEmulatorSet() {
if (!mTerminalCursorBlinkerStateAlreadySet) {
// Start terminal cursor blinking if enabled
// We need to wait for the first session to be attached that's set in
// TermuxActivity.onServiceConnected() and then the multiple calls to TerminalView.updateSize()
// where the final one eventually sets the mEmulator when width/height is not 0. Otherwise
// blinker will not start again if TermuxActivity is started again after exiting it with
// double back press. Check TerminalView.setTerminalCursorBlinkerState().
setTerminalCursorBlinkerState(true);
mTerminalCursorBlinkerStateAlreadySet = true;
}
}
@Override
public float onScale(float scale) {
if (scale < 0.9f || scale > 1.1f) {
boolean increase = scale > 1.f;
changeFontSize(increase);
return 1.0f;
}
return scale;
}
@Override
public void onSingleTapUp(MotionEvent e) {
TerminalEmulator term = mActivity.getCurrentSession().getEmulator();
if (mActivity.getProperties().shouldOpenTerminalTranscriptURLOnClick()) {
int[] columnAndRow = mActivity.getTerminalView().getColumnAndRow(e, true);
String wordAtTap = term.getScreen().getWordAtLocation(columnAndRow[0], columnAndRow[1]);
LinkedHashSet<CharSequence> urlSet = TermuxUrlUtils.extractUrls(wordAtTap);
if (!urlSet.isEmpty()) {
String url = (String) urlSet.iterator().next();
ShareUtils.openUrl(mActivity, url);
return;
}
}
if (!term.isMouseTrackingActive() && !e.isFromSource(InputDevice.SOURCE_MOUSE)) {
if (!KeyboardUtils.areDisableSoftKeyboardFlagsSet(mActivity))
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
else
Logger.logVerbose(LOG_TAG, "Not showing soft keyboard onSingleTapUp since its disabled");
}
}
@Override
public boolean shouldBackButtonBeMappedToEscape() {
return mActivity.getProperties().isBackKeyTheEscapeKey();
}
@Override
public boolean shouldEnforceCharBasedInput() {
return mActivity.getProperties().isEnforcingCharBasedInput();
}
@Override
public boolean shouldUseCtrlSpaceWorkaround() {
return mActivity.getProperties().isUsingCtrlSpaceWorkaround();
}
@Override
public boolean isTerminalViewSelected() {
return mActivity.getTerminalToolbarViewPager() == null || mActivity.isTerminalViewSelected() || mActivity.getTerminalView().hasFocus();
}
@Override
public void copyModeChanged(boolean copyMode) {
// Disable drawer while copying.
mActivity.getDrawer().setDrawerLockMode(copyMode ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED : DrawerLayout.LOCK_MODE_UNLOCKED);
}
@SuppressLint("RtlHardcoded")
@Override
public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession currentSession) {
if (handleVirtualKeys(keyCode, e, true)) return true;
if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) {
mTermuxTerminalSessionActivityClient.removeFinishedSession(currentSession);
return true;
} else if (!mActivity.getProperties().areHardwareKeyboardShortcutsDisabled() &&
e.isCtrlPressed() && e.isAltPressed()) {
// Get the unmodified code point:
int unicodeChar = e.getUnicodeChar(0);
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || unicodeChar == 'n'/* next */) {
mTermuxTerminalSessionActivityClient.switchToSession(true);
} else if (keyCode == KeyEvent.KEYCODE_DPAD_UP || unicodeChar == 'p' /* previous */) {
mTermuxTerminalSessionActivityClient.switchToSession(false);
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
mActivity.getDrawer().openDrawer(Gravity.LEFT);
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
mActivity.getDrawer().closeDrawers();
} else if (unicodeChar == 'k'/* keyboard */) {
onToggleSoftKeyboardRequest();
} else if (unicodeChar == 'm'/* menu */) {
mActivity.getTerminalView().showContextMenu();
} else if (unicodeChar == 'r'/* rename */) {
mTermuxTerminalSessionActivityClient.renameSession(currentSession);
} else if (unicodeChar == 'c'/* create */) {
mTermuxTerminalSessionActivityClient.addNewSession(false, null);
} else if (unicodeChar == 'u' /* urls */) {
showUrlSelection();
} else if (unicodeChar == 'v') {
doPaste();
} 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);
} else if (unicodeChar >= '1' && unicodeChar <= '9') {
int index = unicodeChar - '1';
mTermuxTerminalSessionActivityClient.switchToSession(index);
}
return true;
}
return false;
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent e) {
// If emulator is not set, like if bootstrap installation failed and user dismissed the error
// dialog, then just exit the activity, otherwise they will be stuck in a broken state.
if (keyCode == KeyEvent.KEYCODE_BACK && mActivity.getTerminalView().mEmulator == null) {
mActivity.finishActivityIfNotFinishing();
return true;
}
return handleVirtualKeys(keyCode, e, false);
}
/** Handle dedicated volume buttons as virtual keys if applicable. */
private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) {
InputDevice inputDevice = event.getDevice();
if (mActivity.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) {
mVirtualControlKeyDown = down;
return true;
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
mVirtualFnKeyDown = down;
return true;
}
return false;
}
@Override
public boolean readControlKey() {
return readExtraKeysSpecialButton(SpecialButton.CTRL) || mVirtualControlKeyDown;
}
@Override
public boolean readAltKey() {
return readExtraKeysSpecialButton(SpecialButton.ALT);
}
@Override
public boolean readShiftKey() {
return readExtraKeysSpecialButton(SpecialButton.SHIFT);
}
@Override
public boolean readFnKey() {
return readExtraKeysSpecialButton(SpecialButton.FN);
}
public boolean readExtraKeysSpecialButton(SpecialButton specialButton) {
if (mActivity.getExtraKeysView() == null) return false;
Boolean state = mActivity.getExtraKeysView().readSpecialButton(specialButton, true);
if (state == null) {
Logger.logError(LOG_TAG,"Failed to read an unregistered " + specialButton + " special button value from extra keys.");
return false;
}
return state;
}
@Override
public boolean onLongPress(MotionEvent event) {
return false;
}
@Override
public boolean onCodePoint(final int codePoint, boolean ctrlDown, TerminalSession session) {
if (mVirtualFnKeyDown) {
int resultingKeyCode = -1;
int resultingCodePoint = -1;
boolean altDown = false;
int lowerCase = Character.toLowerCase(codePoint);
switch (lowerCase) {
// Arrow keys.
case 'w':
resultingKeyCode = KeyEvent.KEYCODE_DPAD_UP;
break;
case 'a':
resultingKeyCode = KeyEvent.KEYCODE_DPAD_LEFT;
break;
case 's':
resultingKeyCode = KeyEvent.KEYCODE_DPAD_DOWN;
break;
case 'd':
resultingKeyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
break;
// Page up and down.
case 'p':
resultingKeyCode = KeyEvent.KEYCODE_PAGE_UP;
break;
case 'n':
resultingKeyCode = KeyEvent.KEYCODE_PAGE_DOWN;
break;
// Some special keys:
case 't':
resultingKeyCode = KeyEvent.KEYCODE_TAB;
break;
case 'i':
resultingKeyCode = KeyEvent.KEYCODE_INSERT;
break;
case 'h':
resultingCodePoint = '~';
break;
// Special characters to input.
case 'u':
resultingCodePoint = '_';
break;
case 'l':
resultingCodePoint = '|';
break;
// Function keys.
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
resultingKeyCode = (codePoint - '1') + KeyEvent.KEYCODE_F1;
break;
case '0':
resultingKeyCode = KeyEvent.KEYCODE_F10;
break;
// Other special keys.
case 'e':
resultingCodePoint = /*Escape*/ 27;
break;
case '.':
resultingCodePoint = /*^.*/ 28;
break;
case 'b': // alt+b, jumping backward in readline.
case 'f': // alf+f, jumping forward in readline.
case 'x': // alt+x, common in emacs.
resultingCodePoint = lowerCase;
altDown = true;
break;
// Volume control.
case 'v':
resultingCodePoint = -1;
AudioManager audio = (AudioManager) mActivity.getSystemService(Context.AUDIO_SERVICE);
audio.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME, AudioManager.USE_DEFAULT_STREAM_TYPE, AudioManager.FLAG_SHOW_UI);
break;
// Writing mode:
case 'q':
case 'k':
mActivity.toggleTerminalToolbar();
mVirtualFnKeyDown=false; // force disable fn key down to restore keyboard input into terminal view, fixes termux/termux-app#1420
break;
}
if (resultingKeyCode != -1) {
TerminalEmulator term = session.getEmulator();
session.write(KeyHandler.getCode(resultingKeyCode, 0, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode()));
} else if (resultingCodePoint != -1) {
session.writeCodePoint(altDown, resultingCodePoint);
}
return true;
} else if (ctrlDown) {
if (codePoint == 106 /* Ctrl+j or \n */ && !session.isRunning()) {
mTermuxTerminalSessionActivityClient.removeFinishedSession(session);
return true;
}
List<KeyboardShortcut> shortcuts = mSessionShortcuts;
if (shortcuts != null && !shortcuts.isEmpty()) {
int codePointLowerCase = Character.toLowerCase(codePoint);
for (int i = shortcuts.size() - 1; i >= 0; i--) {
KeyboardShortcut shortcut = shortcuts.get(i);
if (codePointLowerCase == shortcut.codePoint) {
switch (shortcut.shortcutAction) {
case TermuxPropertyConstants.ACTION_SHORTCUT_CREATE_SESSION:
mTermuxTerminalSessionActivityClient.addNewSession(false, null);
return true;
case TermuxPropertyConstants.ACTION_SHORTCUT_NEXT_SESSION:
mTermuxTerminalSessionActivityClient.switchToSession(true);
return true;
case TermuxPropertyConstants.ACTION_SHORTCUT_PREVIOUS_SESSION:
mTermuxTerminalSessionActivityClient.switchToSession(false);
return true;
case TermuxPropertyConstants.ACTION_SHORTCUT_RENAME_SESSION:
mTermuxTerminalSessionActivityClient.renameSession(mActivity.getCurrentSession());
return true;
}
}
}
}
}
return false;
}
/**
* Set the terminal sessions shortcuts.
*/
private void setSessionShortcuts() {
mSessionShortcuts = new ArrayList<>();
// The {@link TermuxPropertyConstants#MAP_SESSION_SHORTCUTS} stores the session shortcut key and action pair
for (Map.Entry<String, Integer> entry : TermuxPropertyConstants.MAP_SESSION_SHORTCUTS.entrySet()) {
// The mMap stores the code points for the session shortcuts while loading properties
Integer codePoint = (Integer) mActivity.getProperties().getInternalPropertyValue(entry.getKey(), true);
// If codePoint is null, then session shortcut did not exist in properties or was invalid
// as parsed by {@link #getCodePointForSessionShortcuts(String,String)}
// If codePoint is not null, then get the action for the MAP_SESSION_SHORTCUTS key and
// add the code point to sessionShortcuts
if (codePoint != null)
mSessionShortcuts.add(new KeyboardShortcut(codePoint, entry.getValue()));
}
}
public void changeFontSize(boolean increase) {
mActivity.getPreferences().changeFontSize(increase);
mActivity.getTerminalView().setTextSize(mActivity.getPreferences().getFontSize());
}
/**
* Called when user requests the soft keyboard to be toggled via "KEYBOARD" toggle button in
* drawer or extra keys, or with ctrl+alt+k hardware keyboard shortcut.
*/
public void onToggleSoftKeyboardRequest() {
// If soft keyboard toggle behaviour is enable/disabled
if (mActivity.getProperties().shouldEnableDisableSoftKeyboardOnToggle()) {
// If soft keyboard is visible
if (!KeyboardUtils.areDisableSoftKeyboardFlagsSet(mActivity)) {
Logger.logVerbose(LOG_TAG, "Disabling soft keyboard on toggle");
mActivity.getPreferences().setSoftKeyboardEnabled(false);
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
} else {
// Show with a delay, otherwise pressing keyboard toggle won't show the keyboard after
// switching back from another app if keyboard was previously disabled by user.
// Also request focus, since it wouldn't have been requested at startup by
// setSoftKeyboardState if keyboard was disabled. #2112
Logger.logVerbose(LOG_TAG, "Enabling soft keyboard on toggle");
mActivity.getPreferences().setSoftKeyboardEnabled(true);
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
if(mShowSoftKeyboardWithDelayOnce) {
mShowSoftKeyboardWithDelayOnce = false;
mActivity.getTerminalView().postDelayed(getShowSoftKeyboardRunnable(), 500);
mActivity.getTerminalView().requestFocus();
} else
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
}
}
// If soft keyboard toggle behaviour is show/hide
else {
// If soft keyboard is disabled by user for Termux
if (!mActivity.getPreferences().isSoftKeyboardEnabled()) {
Logger.logVerbose(LOG_TAG, "Maintaining disabled soft keyboard on toggle");
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
} else {
Logger.logVerbose(LOG_TAG, "Showing/Hiding soft keyboard on toggle");
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
KeyboardUtils.toggleSoftKeyboard(mActivity);
}
}
}
public void setSoftKeyboardState(boolean isStartup, boolean isReloadTermuxProperties) {
boolean noShowKeyboard = false;
// Requesting terminal view focus is necessary regardless of if soft keyboard is to be
// disabled or hidden at startup, otherwise if hardware keyboard is attached and user
// starts typing on hardware keyboard without tapping on the terminal first, then a colour
// tint will be added to the terminal as highlight for the focussed view. Test with a light
// theme. For android 8.+, the "defaultFocusHighlightEnabled" attribute is also set to false
// in TerminalView layout to fix the issue.
// If soft keyboard is disabled by user for Termux (check function docs for Termux behaviour info)
if (KeyboardUtils.shouldSoftKeyboardBeDisabled(mActivity,
mActivity.getPreferences().isSoftKeyboardEnabled(),
mActivity.getPreferences().isSoftKeyboardEnabledOnlyIfNoHardware())) {
Logger.logVerbose(LOG_TAG, "Maintaining disabled soft keyboard");
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
mActivity.getTerminalView().requestFocus();
noShowKeyboard = true;
// Delay is only required if onCreate() is called like when Termux app is exited with
// double back press, not when Termux app is switched back from another app and keyboard
// toggle is pressed to enable keyboard
if (isStartup && mActivity.isOnResumeAfterOnCreate())
mShowSoftKeyboardWithDelayOnce = true;
} else {
// Set flag to automatically push up TerminalView when keyboard is opened instead of showing over it
KeyboardUtils.setSoftInputModeAdjustResize(mActivity);
// Clear any previous flags to disable soft keyboard in case setting updated
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
// If soft keyboard is to be hidden on startup
if (isStartup && mActivity.getProperties().shouldSoftKeyboardBeHiddenOnStartup()) {
Logger.logVerbose(LOG_TAG, "Hiding soft keyboard on startup");
// Required to keep keyboard hidden when Termux app is switched back from another app
KeyboardUtils.setSoftKeyboardAlwaysHiddenFlags(mActivity);
KeyboardUtils.hideSoftKeyboard(mActivity, mActivity.getTerminalView());
mActivity.getTerminalView().requestFocus();
noShowKeyboard = true;
// Required to keep keyboard hidden on app startup
mShowSoftKeyboardIgnoreOnce = true;
}
}
mActivity.getTerminalView().setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View view, boolean hasFocus) {
// Force show soft keyboard if TerminalView or toolbar text input view has
// focus and close it if they don't
boolean textInputViewHasFocus = false;
final EditText textInputView = mActivity.findViewById(R.id.terminal_toolbar_text_input);
if (textInputView != null) textInputViewHasFocus = textInputView.hasFocus();
if (hasFocus || textInputViewHasFocus) {
if (mShowSoftKeyboardIgnoreOnce) {
mShowSoftKeyboardIgnoreOnce = false; return;
}
Logger.logVerbose(LOG_TAG, "Showing soft keyboard on focus change");
} else {
Logger.logVerbose(LOG_TAG, "Hiding soft keyboard on focus change");
}
KeyboardUtils.setSoftKeyboardVisibility(getShowSoftKeyboardRunnable(), mActivity, mActivity.getTerminalView(), hasFocus || textInputViewHasFocus);
}
});
// Do not force show soft keyboard if termux-reload-settings command was run with hardware keyboard
// or soft keyboard is to be hidden or is disabled
if (!isReloadTermuxProperties && !noShowKeyboard) {
// Request focus for TerminalView
// Also show the keyboard, since onFocusChange will not be called if TerminalView already
// had focus on startup to show the keyboard, like when opening url with context menu
// "Select URL" long press and returning to Termux app with back button. This
// will also show keyboard even if it was closed before opening url. #2111
Logger.logVerbose(LOG_TAG, "Requesting TerminalView focus and showing soft keyboard");
mActivity.getTerminalView().requestFocus();
mActivity.getTerminalView().postDelayed(getShowSoftKeyboardRunnable(), 300);
}
}
private Runnable getShowSoftKeyboardRunnable() {
if (mShowSoftKeyboardRunnable == null) {
mShowSoftKeyboardRunnable = () -> {
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
};
}
return mShowSoftKeyboardRunnable;
}
public void setTerminalCursorBlinkerState(boolean start) {
if (start) {
// If set/update the cursor blinking rate is successful, then enable cursor blinker
if (mActivity.getTerminalView().setTerminalCursorBlinkerRate(mActivity.getProperties().getTerminalCursorBlinkRate()))
mActivity.getTerminalView().setTerminalCursorBlinkerState(true, true);
else
Logger.logError(LOG_TAG,"Failed to start cursor blinker");
} else {
// Disable cursor blinker
mActivity.getTerminalView().setTerminalCursorBlinkerState(false, true);
}
}
public void shareSessionTranscript() {
TerminalSession session = mActivity.getCurrentSession();
if (session == null) return;
String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
if (transcriptText == null) return;
// See https://github.com/termux/termux-app/issues/1166.
transcriptText = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
ShareUtils.shareText(mActivity, mActivity.getString(R.string.title_share_transcript),
transcriptText, mActivity.getString(R.string.title_share_transcript_with));
}
public void shareSelectedText() {
String selectedText = mActivity.getTerminalView().getStoredSelectedText();
if (DataUtils.isNullOrEmpty(selectedText)) return;
ShareUtils.shareText(mActivity, mActivity.getString(R.string.title_share_selected_text),
selectedText, mActivity.getString(R.string.title_share_selected_text_with));
}
public void showUrlSelection() {
TerminalSession session = mActivity.getCurrentSession();
if (session == null) return;
String text = ShellUtils.getTerminalSessionTranscriptText(session, true, true);
LinkedHashSet<CharSequence> urlSet = TermuxUrlUtils.extractUrls(text);
if (urlSet.isEmpty()) {
new AlertDialog.Builder(mActivity).setMessage(R.string.title_select_url_none_found).show();
return;
}
final CharSequence[] urls = urlSet.toArray(new CharSequence[0]);
Collections.reverse(Arrays.asList(urls)); // Latest first.
// Click to copy url to clipboard:
final AlertDialog dialog = new AlertDialog.Builder(mActivity).setItems(urls, (di, which) -> {
String url = (String) urls[which];
ShareUtils.copyTextToClipboard(mActivity, url, mActivity.getString(R.string.msg_select_url_copied_to_clipboard));
}).setTitle(R.string.title_select_url_dialog).create();
// Long press to open URL:
dialog.setOnShowListener(di -> {
ListView lv = dialog.getListView(); // this is a ListView with your "buds" in it
lv.setOnItemLongClickListener((parent, view, position, id) -> {
dialog.dismiss();
String url = (String) urls[position];
ShareUtils.openUrl(mActivity, url);
return true;
});
});
dialog.show();
}
public void reportIssueFromTranscript() {
TerminalSession session = mActivity.getCurrentSession();
if (session == null) return;
final String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
if (transcriptText == null) return;
MessageDialogUtils.showMessage(mActivity, TermuxConstants.TERMUX_APP_NAME + " Report Issue",
mActivity.getString(R.string.msg_add_termux_debug_info),
mActivity.getString(R.string.action_yes), (dialog, which) -> reportIssueFromTranscript(transcriptText, true),
mActivity.getString(R.string.action_no), (dialog, which) -> reportIssueFromTranscript(transcriptText, false),
null);
}
private void reportIssueFromTranscript(String transcriptText, boolean addTermuxDebugInfo) {
Logger.showToast(mActivity, mActivity.getString(R.string.msg_generating_report), true);
new Thread() {
@Override
public void run() {
StringBuilder reportString = new StringBuilder();
String title = TermuxConstants.TERMUX_APP_NAME + " Report Issue";
reportString.append("## Transcript\n");
reportString.append("\n").append(MarkdownUtils.getMarkdownCodeForString(transcriptText, true));
reportString.append("\n##\n");
if (addTermuxDebugInfo) {
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGES));
} else {
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, TermuxUtils.AppInfoMode.TERMUX_PACKAGE));
}
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(mActivity, true));
if (TermuxBootstrap.isAppPackageManagerAPT()) {
String termuxAptInfo = TermuxUtils.geAPTInfoMarkdownString(mActivity);
if (termuxAptInfo != null)
reportString.append("\n\n").append(termuxAptInfo);
}
if (addTermuxDebugInfo) {
String termuxDebugInfo = TermuxUtils.getTermuxDebugMarkdownString(mActivity);
if (termuxDebugInfo != null)
reportString.append("\n\n").append(termuxDebugInfo);
}
String userActionName = UserAction.REPORT_ISSUE_FROM_TRANSCRIPT.getName();
ReportInfo reportInfo = new ReportInfo(userActionName,
TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY_NAME, title);
reportInfo.setReportString(reportString.toString());
reportInfo.setReportStringSuffix("\n\n" + TermuxUtils.getReportIssueMarkdownString(mActivity));
reportInfo.setReportSaveFileLabelAndPath(userActionName,
Environment.getExternalStorageDirectory() + "/" +
FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true));
ReportActivity.startReportActivity(mActivity, reportInfo);
}
}.start();
}
public void doPaste() {
TerminalSession session = mActivity.getCurrentSession();
if (session == null) return;
if (!session.isRunning()) return;
String text = ShareUtils.getTextStringFromClipboardIfSet(mActivity, true);
if (text != null)
session.getEmulator().paste(text);
}
}

View File

@ -1,68 +0,0 @@
package com.termux.app.terminal.io;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewGroup;
import com.termux.app.TermuxActivity;
/**
* Work around for fullscreen mode in Termux to fix ExtraKeysView not being visible.
* This class is derived from:
* https://stackoverflow.com/questions/7417123/android-how-to-adjust-layout-in-full-screen-mode-when-softkeyboard-is-visible
* and has some additional tweaks
* ---
* For more information, see https://issuetracker.google.com/issues/36911528
*/
public class FullScreenWorkAround {
private final View mChildOfContent;
private int mUsableHeightPrevious;
private final ViewGroup.LayoutParams mViewGroupLayoutParams;
private final int mNavBarHeight;
public static void apply(TermuxActivity activity) {
new FullScreenWorkAround(activity);
}
private FullScreenWorkAround(TermuxActivity activity) {
ViewGroup content = activity.findViewById(android.R.id.content);
mChildOfContent = content.getChildAt(0);
mViewGroupLayoutParams = mChildOfContent.getLayoutParams();
mNavBarHeight = activity.getNavBarHeight();
mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(this::possiblyResizeChildOfContent);
}
private void possiblyResizeChildOfContent() {
int usableHeightNow = computeUsableHeight();
if (usableHeightNow != mUsableHeightPrevious) {
int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
int heightDifference = usableHeightSansKeyboard - usableHeightNow;
if (heightDifference > (usableHeightSansKeyboard / 4)) {
// keyboard probably just became visible
// ensures that usable layout space does not extend behind the
// soft keyboard, causing the extra keys to not be visible
mViewGroupLayoutParams.height = (usableHeightSansKeyboard - heightDifference) + getNavBarHeight();
} else {
// keyboard probably just became hidden
mViewGroupLayoutParams.height = usableHeightSansKeyboard;
}
mChildOfContent.requestLayout();
mUsableHeightPrevious = usableHeightNow;
}
}
private int getNavBarHeight() {
return mNavBarHeight;
}
private int computeUsableHeight() {
Rect r = new Rect();
mChildOfContent.getWindowVisibleDisplayFrame(r);
return (r.bottom - r.top);
}
}

View File

@ -1,108 +0,0 @@
package com.termux.app.terminal.io;
import android.annotation.SuppressLint;
import android.view.Gravity;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.drawerlayout.widget.DrawerLayout;
import com.termux.app.TermuxActivity;
import com.termux.app.terminal.TermuxTerminalSessionActivityClient;
import com.termux.app.terminal.TermuxTerminalViewClient;
import com.termux.shared.logger.Logger;
import com.termux.shared.termux.extrakeys.ExtraKeysConstants;
import com.termux.shared.termux.extrakeys.ExtraKeysInfo;
import com.termux.shared.termux.settings.properties.TermuxPropertyConstants;
import com.termux.shared.termux.settings.properties.TermuxSharedProperties;
import com.termux.shared.termux.terminal.io.TerminalExtraKeys;
import com.termux.view.TerminalView;
import org.json.JSONException;
public class TermuxTerminalExtraKeys extends TerminalExtraKeys {
private ExtraKeysInfo mExtraKeysInfo;
final TermuxActivity mActivity;
final TermuxTerminalViewClient mTermuxTerminalViewClient;
final TermuxTerminalSessionActivityClient mTermuxTerminalSessionActivityClient;
private static final String LOG_TAG = "TermuxTerminalExtraKeys";
public TermuxTerminalExtraKeys(TermuxActivity activity, @NonNull TerminalView terminalView,
TermuxTerminalViewClient termuxTerminalViewClient,
TermuxTerminalSessionActivityClient termuxTerminalSessionActivityClient) {
super(terminalView);
mActivity = activity;
mTermuxTerminalViewClient = termuxTerminalViewClient;
mTermuxTerminalSessionActivityClient = termuxTerminalSessionActivityClient;
setExtraKeys();
}
/**
* Set the terminal extra keys and style.
*/
private void setExtraKeys() {
mExtraKeysInfo = null;
try {
// The mMap stores the extra key and style string values while loading properties
// Check {@link #getExtraKeysInternalPropertyValueFromValue(String)} and
// {@link #getExtraKeysStyleInternalPropertyValueFromValue(String)}
String extrakeys = (String) mActivity.getProperties().getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS, true);
String extraKeysStyle = (String) mActivity.getProperties().getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE, true);
ExtraKeysConstants.ExtraKeyDisplayMap extraKeyDisplayMap = ExtraKeysInfo.getCharDisplayMapForStyle(extraKeysStyle);
if (ExtraKeysConstants.EXTRA_KEY_DISPLAY_MAPS.DEFAULT_CHAR_DISPLAY.equals(extraKeyDisplayMap) && !TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE.equals(extraKeysStyle)) {
Logger.logError(TermuxSharedProperties.LOG_TAG, "The style \"" + extraKeysStyle + "\" for the key \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE + "\" is invalid. Using default style instead.");
extraKeysStyle = TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE;
}
mExtraKeysInfo = new ExtraKeysInfo(extrakeys, extraKeysStyle, ExtraKeysConstants.CONTROL_CHARS_ALIASES);
} catch (JSONException e) {
Logger.showToast(mActivity, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: " + e.toString(), true);
Logger.logStackTraceWithMessage(LOG_TAG, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: ", e);
try {
mExtraKeysInfo = new ExtraKeysInfo(TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE, ExtraKeysConstants.CONTROL_CHARS_ALIASES);
} catch (JSONException e2) {
Logger.showToast(mActivity, "Can't create default extra keys",true);
Logger.logStackTraceWithMessage(LOG_TAG, "Could create default extra keys: ", e);
mExtraKeysInfo = null;
}
}
}
public ExtraKeysInfo getExtraKeysInfo() {
return mExtraKeysInfo;
}
@SuppressLint("RtlHardcoded")
@Override
public void onTerminalExtraKeyButtonClick(View view, String key, boolean ctrlDown, boolean altDown, boolean shiftDown, boolean fnDown) {
if ("KEYBOARD".equals(key)) {
if(mTermuxTerminalViewClient != null)
mTermuxTerminalViewClient.onToggleSoftKeyboardRequest();
} else if ("DRAWER".equals(key)) {
DrawerLayout drawerLayout = mTermuxTerminalViewClient.getActivity().getDrawer();
if (drawerLayout.isDrawerOpen(Gravity.LEFT))
drawerLayout.closeDrawer(Gravity.LEFT);
else
drawerLayout.openDrawer(Gravity.LEFT);
} else if ("PASTE".equals(key)) {
if(mTermuxTerminalSessionActivityClient != null)
mTermuxTerminalSessionActivityClient.onPasteTextFromClipboard(null);
} else if ("SCROLL".equals(key)) {
TerminalView terminalView = mTermuxTerminalViewClient.getActivity().getTerminalView();
if (terminalView != null && terminalView.mEmulator != null)
terminalView.mEmulator.toggleAutoScrollDisabled();
} else {
super.onTerminalExtraKeyButtonClick(view, key, ctrlDown, altDown, shiftDown, fnDown);
}
}
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include
layout="@layout/partial_primary_toolbar"
android:id="@+id/partial_primary_toolbar"/>
<FrameLayout
android:id="@+id/settings"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@ -1,20 +1,13 @@
<com.termux.app.terminal.TermuxActivityRootView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_termux_root_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:fitsSystemWindows="true">
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_termux_root_relative_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginHorizontal="3dp"
android:layout_marginVertical="0dp"
android:orientation="vertical">
android:layout_height="match_parent"
android:orientation="vertical"
android:fitsSystemWindows="true"
>
<androidx.drawerlayout.widget.DrawerLayout
android:id="@+id/drawer_layout"
@ -101,15 +94,7 @@
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="37.5dp"
android:background="@color/black"
android:background="@android:color/black"
android:layout_alignParentBottom="true" />
</RelativeLayout>
<View
android:id="@+id/activity_termux_bottom_space_view"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@android:color/transparent" />
</com.termux.app.terminal.TermuxActivityRootView>

View File

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

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<com.termux.shared.termux.extrakeys.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
<com.termux.app.extrakeys.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/terminal_toolbar_extra_keys"
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"

View File

@ -11,13 +11,9 @@
<!-- Primary brand color. -->
<item name="colorPrimary">@color/black</item>
<item name="colorPrimaryVariant">@color/black</item>
-l
<item name="android:windowBackground">@color/black</item>
<!-- Avoid action mode toolbar pushing down terminal content when
selecting text on pre-6.0 (non-floating toolbar). -->
<item name="android:windowActionModeOverlay">true</item>
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowTranslucentNavigation">true</item>

View File

@ -2,4 +2,9 @@
<resources>
<attr name="termuxActivityDrawerBackground" format="reference" />
<attr name="termuxActivityDrawerImageTint" format="reference" />
<attr name="extraKeysButtonTextColor" format="reference" />
<attr name="extraKeysButtonActiveTextColor" format="reference" />
<attr name="extraKeysButtonBackgroundColor" format="reference" />
<attr name="extraKeysButtonActiveBackgroundColor" format="reference" />
</resources>

View File

@ -1,3 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="background_markdown_code_inline">#1F000000</color>
<color name="background_markdown_code_block">#0F000000</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="red_400">#FF0000</color>
<color name="red_800">#C4001D</color>
<color name="grey_200">#EEEEEE</color>
<color name="grey_400">#BDBDBD</color>
<color name="grey_500">#9E9E9E</color>
<color name="grey_800">#424242</color>
<color name="grey_900">#212121</color>
<color name="red_error">#DC143C</color>
<color name="red_error_link">#FC143C</color>
<color name="blue_link_light">#0969DA</color>
<color name="blue_link_dark">#58A6FF</color>
</resources>

View File

@ -1,237 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE resources [
<!ENTITY TERMUX_PACKAGE_NAME "com.termux">
<!ENTITY TERMUX_APP_NAME "Termux">
<!ENTITY TERMUX_API_APP_NAME "Termux:API">
<!ENTITY TERMUX_BOOT_APP_NAME "Termux:Boot">
<!ENTITY TERMUX_FLOAT_APP_NAME "Termux:Float">
<!ENTITY TERMUX_STYLING_APP_NAME "Termux:Styling">
<!ENTITY TERMUX_TASKER_APP_NAME "Termux:Tasker">
<!ENTITY TERMUX_WIDGET_APP_NAME "Termux:Widget">
]>
<resources>
<string name="application_name">&TERMUX_APP_NAME;</string>
<string name="shared_user_label">&TERMUX_APP_NAME; user</string>
<string name="application_name">Termux</string>
<string name="shared_user_label">Termux user</string>
<!-- Termux RUN_COMMAND permission -->
<string name="permission_run_command_label">Run commands in &TERMUX_APP_NAME; environment</string>
<string name="permission_run_command_description">execute arbitrary commands within &TERMUX_APP_NAME;
environment and access files</string>
<string name="permission_run_command_label">Run commands in the Termux environment</string>
<string name="permission_run_command_description">execute arbitrary commands within Termux environment and access files</string>
<!-- Termux Bootstrap Packages Installation -->
<string name="bootstrap_installer_body">Installing bootstrap packages…</string>
<string name="bootstrap_error_title">Unable to install bootstrap</string>
<string name="bootstrap_error_body">&TERMUX_APP_NAME; was unable to install the bootstrap packages.</string>
<string name="bootstrap_error_body">Termux was unable to install the bootstrap packages.</string>
<string name="bootstrap_error_abort">Abort</string>
<string name="bootstrap_error_try_again">Try again</string>
<string name="bootstrap_error_not_primary_user_message">&TERMUX_APP_NAME; can only be run as the primary user.
\nBootstrap binaries compiled for &TERMUX_APP_NAME; have hardcoded $PREFIX path and cannot be installed
under any path other than:\n%1$s.</string>
<string name="bootstrap_error_installed_on_portable_sd">&TERMUX_APP_NAME; cannot be installed on
portable/external/removable sd card on your device.
\nBootstrap binaries compiled for &TERMUX_APP_NAME; have hardcoded $PREFIX path and cannot be installed
under any path other than:\n%1$s.</string>
<string name="bootstrap_error_not_primary_user_message">Termux can only be run as the primary user.</string>
<string name="bootstrap_error_installed_on_portable_sd">Termux cannot be installed on external storage.</string>
<!-- Terminal Sidebar and Shortcuts -->
<string name="action_new_session">New session</string>
<string name="action_new_session_failsafe">Failsafe</string>
<string name="title_max_terminals_reached">Max terminals reached</string>
<string name="msg_max_terminals_reached">Close down existing ones before creating new.</string>
<string name="title_rename_session">Set session name</string>
<string name="action_rename_session_confirm">Set</string>
<string name="title_create_named_session">New named session</string>
<string name="action_create_named_session_confirm">Create</string>
<string name="action_toggle_soft_keyboard">Keyboard</string>
<string name="msg_enabling_terminal_toolbar">Enabling Terminal Toolbar</string>
<string name="msg_disabling_terminal_toolbar">Disabling Terminal Toolbar</string>
<!-- Terminal Popup -->
<string name="action_select_url">Select URL</string>
<string name="title_select_url_dialog">Click URL to copy or long press to open</string>
<string name="title_select_url_none_found">No URL found in the terminal.</string>
<string name="msg_select_url_copied_to_clipboard">URL copied to clipboard</string>
<string name="action_share_transcript">Share transcript</string>
<string name="title_share_transcript">Terminal transcript</string>
<string name="title_share_transcript_with">Send transcript to:</string>
<string name="action_share_selected_text">Share selected text</string>
<string name="title_share_selected_text">Terminal Text</string>
<string name="title_share_selected_text_with">Send selected text to:</string>
<string name="action_autofill_password">Autofill password</string>
<string name="action_reset_terminal">Reset</string>
<string name="msg_terminal_reset">Terminal reset</string>
<string name="action_kill_process">Kill process (%d)</string>
<string name="title_confirm_kill_process">Really kill this session?</string>
<string name="action_style_terminal">Style</string>
<string name="action_toggle_keep_screen_on">Keep screen on</string>
<string name="action_open_help">Help</string>
<string name="action_open_settings">Settings</string>
<string name="action_report_issue">Report Issue</string>
<string name="msg_generating_report">Generating Report</string>
<string name="msg_add_termux_debug_info">Add termux debug info to report?</string>
<string name="error_styling_not_installed">The &TERMUX_STYLING_APP_NAME; Plugin App is not installed.</string>
<string name="error_styling_not_installed">The Termux:Styling Plugin App is not installed.</string>
<string name="action_styling_install">Install</string>
<!-- Share -->
<string name="title_share_with">Share With</string>
<string name="title_open_url_with">Open URL With</string>
<string name="msg_storage_permission_not_granted">The storage permission not granted.</string>
<string name="msg_file_saved_successfully">The %1$s file saved successfully at \"%2$s\"</string>
<!-- Termux Notifications -->
<string name="notification_action_exit">Exit</string>
<string name="notification_action_wake_lock">Acquire wakelock</string>
<string name="notification_action_wake_unlock">Release wakelock</string>
<string name="msg_storage_permission_granted_on_request">The storage permission granted by user on request</string>
<string name="msg_storage_permission_not_granted_on_request">The storage permission not granted by user on request</string>
<!-- TermuxService -->
<string name="error_display_over_other_apps_permission_not_granted_to_start_terminal">&TERMUX_APP_NAME; requires
\"Display over other apps\" permission to start terminal sessions from background on Android >= 10.
Grants it from Settings -> Apps -> &TERMUX_APP_NAME; -> Advanced</string>
<string name="error_termux_service_invalid_execution_command_runner">Invalid execution command runner to TermuxService: `%1$s`</string>
<string name="error_termux_service_unsupported_execution_command_runner">Unsupported execution command runner to TermuxService: `%1$s`</string>
<string name="error_termux_service_unsupported_execution_command_shell_create_mode">Unsupported execution command shell create mode to TermuxService: `%1$s`</string>
<string name="error_termux_service_execution_command_shell_name_unset">Shell name not set but `%1$s` shell create mode passed</string>
<!-- Termux RunCommandService -->
<string name="error_run_command_service_invalid_intent_action">Invalid intent action to RunCommandService: `%1$s`</string>
<string name="error_run_command_service_invalid_execution_command_runner">Invalid execution command runner to RunCommandService: `%1$s`</string>
<string name="error_run_command_service_mandatory_extra_missing">Mandatory extra missing to RunCommandService: \"%1$s\"</string>
<string name="error_run_command_service_api_help">Visit %1$s for more info on RUN_COMMAND Intent usage.</string>
<!-- Termux File Receiver -->
<string name="title_file_received">Save file in ~/downloads/</string>
<string name="action_file_received_edit">Edit</string>
<string name="action_file_received_open_directory">Open directory</string>
<!-- Miscellaneous -->
<string name="error_termux_service_start_failed_general">Failed to start TermuxService. Check logcat for exception message.</string>
<string name="error_termux_service_start_failed_bg">Failed to start TermuxService while app is in background due to android bg restrictions.</string>
<!-- Termux Settings -->
<string name="title_activity_termux_settings">&TERMUX_APP_NAME; Settings</string>
<!-- Termux App Preferences -->
<string name="termux_preferences_title">&TERMUX_APP_NAME;</string>
<string name="termux_preferences_summary">Preferences for &TERMUX_APP_NAME; app</string>
<!-- Debugging Preferences -->
<string name="termux_debugging_preferences_title">Debugging</string>
<string name="termux_debugging_preferences_summary">Preferences for debugging</string>
<!-- Logging Category -->
<string name="termux_logging_header">Logging</string>
<!-- Log Level -->
<string name="termux_log_level_title">Log Level</string>
<!-- Terminal View Key Logging -->
<string name="termux_terminal_view_key_logging_enabled_title">Terminal View Key Logging</string>
<string name="termux_terminal_view_key_logging_enabled_off">Logs will not have entries for terminal view keys. (Default)</string>
<string name="termux_terminal_view_key_logging_enabled_on">Logcat logs will have entries for terminal view keys.
These are very verbose and should be disabled under normal circumstances or will cause performance issues.</string>
<!-- Plugin Error Notifications -->
<string name="termux_plugin_error_notifications_enabled_title">Plugin Error Notifications</string>
<string name="termux_plugin_error_notifications_enabled_off">Disable flashes and notifications for plugin errors.</string>
<string name="termux_plugin_error_notifications_enabled_on">Show flashes and notifications for plugin errors. (Default)</string>
<!-- Crash Report Notifications -->
<string name="termux_crash_report_notifications_enabled_title">Crash Report Notifications</string>
<string name="termux_crash_report_notifications_enabled_off">Disable notifications for crash reports.</string>
<string name="termux_crash_report_notifications_enabled_on">Show notifications for crash reports. (Default)</string>
<!-- Terminal IO Preferences -->
<string name="termux_terminal_io_preferences_title">Terminal I/O</string>
<string name="termux_terminal_io_preferences_summary">Preferences for terminal I/O</string>
<!-- Keyboard Category -->
<string name="termux_keyboard_header">Keyboard</string>
<!-- Soft Keyboard -->
<string name="termux_soft_keyboard_enabled_title">Soft Keyboard Enabled</string>
<string name="termux_soft_keyboard_enabled_off">Soft keyboard will be disabled.</string>
<string name="termux_soft_keyboard_enabled_on">Soft keyboard will be enabled. (Default)</string>
<!-- Soft Keyboard Only If No Hardware-->
<string name="termux_soft_keyboard_enabled_only_if_no_hardware_title">Soft Keyboard Only If No Hardware</string>
<string name="termux_soft_keyboard_enabled_only_if_no_hardware_off">Soft keyboard will be enabled even if
hardware keyboard is connected. (Default)</string>
<string name="termux_soft_keyboard_enabled_only_if_no_hardware_on">Soft keyboard will be enabled only if
no hardware keyboard is connected.</string>
<!-- Terminal View Preferences -->
<string name="termux_terminal_view_preferences_title">Terminal View</string>
<string name="termux_terminal_view_preferences_summary">Preferences for terminal view</string>
<!-- View Category -->
<string name="termux_terminal_view_view_header">View</string>
<!-- Terminal View Margin Adjustment -->
<string name="termux_terminal_view_terminal_margin_adjustment_title">Terminal Margin Adjustment</string>
<string name="termux_terminal_view_terminal_margin_adjustment_off">Terminal margin adjustment will be disabled.</string>
<string name="termux_terminal_view_terminal_margin_adjustment_on">Terminal margin adjustment will be enabled.
It should be enabled to try to fix the issue where soft keyboard covers part of extra keys/terminal view.
If it causes screen flickering on your devices, then disable it. (Default)</string>
<!-- Termux:API App Preferences -->
<string name="termux_api_preferences_title">&TERMUX_API_APP_NAME;</string>
<string name="termux_api_preferences_summary">Preferences for &TERMUX_API_APP_NAME; app</string>
<!-- Termux:Float App Preferences -->
<string name="termux_float_preferences_title">&TERMUX_FLOAT_APP_NAME;</string>
<string name="termux_float_preferences_summary">Preferences for &TERMUX_FLOAT_APP_NAME; app</string>
<!-- Termux:Tasker App Preferences -->
<string name="termux_tasker_preferences_title">&TERMUX_TASKER_APP_NAME;</string>
<string name="termux_tasker_preferences_summary">Preferences for &TERMUX_TASKER_APP_NAME; app</string>
<!-- Termux:Widget App Preferences -->
<string name="termux_widget_preferences_title">&TERMUX_WIDGET_APP_NAME;</string>
<string name="termux_widget_preferences_summary">Preferences for &TERMUX_WIDGET_APP_NAME; app</string>
<!-- About Preference -->
<string name="about_preference_title">About</string>
<!-- Donate Preference -->
<string name="donate_preference_title">Donate</string>
</resources>

View File

@ -7,11 +7,11 @@
</style>
<style name="TermuxActivity.Drawer.ButtonBarStyle.Light" parent="@style/Widget.MaterialComponents.Button.TextButton">
<item name="android:textColor">@color/black</item>
<item name="android:textColor">@android:color/black</item>
</style>
<style name="TermuxActivity.Drawer.ButtonBarStyle.Dark" parent="@style/Widget.MaterialComponents.Button.TextButton">
<item name="android:textColor">@color/white</item>
<item name="android:textColor">@android:color/white</item>
</style>
</resources>

View File

@ -4,16 +4,8 @@
https://material.io/develop/android/theming/dark
-->
<!-- TermuxApp Light DarkActionBar theme. -->
<style name="Theme.TermuxApp.Light.DarkActionBar" parent="Theme.BaseActivity.Light.DarkActionBar"/>
<!-- TermuxApp Light NoActionBar theme. -->
<style name="Theme.TermuxApp.Light.NoActionBar" parent="Theme.BaseActivity.Light.NoActionBar"/>
<!-- TermuxApp DayNight DarkActionBar theme. -->
<style name="Theme.TermuxApp.DayNight.DarkActionBar" parent="Theme.BaseActivity.DayNight.DarkActionBar"/>
<!-- TermuxApp DayNight NoActionBar theme. -->
<style name="Theme.TermuxApp.DayNight.NoActionBar" parent="Theme.BaseActivity.DayNight.NoActionBar"/>
<style name="Theme.TermuxApp.DayNight.NoActionBar" parent="Theme.MaterialComponents.NoActionBar"/>
<!-- TermuxActivity DayNight NoActionBar theme. -->
<style name="Theme.TermuxActivity.DayNight.NoActionBar" parent="Theme.TermuxApp.DayNight.NoActionBar">
@ -23,10 +15,6 @@
<item name="android:windowBackground">@color/black</item>
<!-- Avoid action mode toolbar pushing down terminal content when
selecting text on pre-6.0 (non-floating toolbar). -->
<item name="android:windowActionModeOverlay">true</item>
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowTranslucentNavigation">true</item>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,5 @@
package com.termux.app;
import com.termux.shared.termux.data.TermuxUrlUtils;
import org.junit.Assert;
import org.junit.Test;

View File

@ -1,6 +1,4 @@
package com.termux.app.api.file;
import com.termux.app.api.file.FileReceiverActivity;
package com.termux.app;
import org.junit.Assert;
import org.junit.Test;
@ -11,7 +9,7 @@ import java.util.ArrayList;
import java.util.List;
@RunWith(RobolectricTestRunner.class)
public class FileReceiverActivityTest {
public class TermuxFileReceiverActivityTest {
@Test
public void testIsSharedTextAnUrl() {
@ -21,13 +19,13 @@ public class FileReceiverActivityTest {
validUrls.add("https://example.com/path/parameter=foo");
validUrls.add("magnet:?xt=urn:btih:d540fc48eb12f2833163eed6421d449dd8f1ce1f&dn=Ubuntu+desktop+19.04+%2864bit%29&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=udp%3A%2F%2Ftracker.publicbt.com%3A80&tr=udp%3A%2F%2Ftracker.ccc.de%3A80");
for (String url : validUrls) {
Assert.assertTrue(FileReceiverActivity.isSharedTextAnUrl(url));
Assert.assertTrue(TermuxFileReceiverActivity.isSharedTextAnUrl(url));
}
List<String> invalidUrls = new ArrayList<>();
invalidUrls.add("a test with example.com");
for (String url : invalidUrls) {
Assert.assertFalse(FileReceiverActivity.isSharedTextAnUrl(url));
Assert.assertFalse(TermuxFileReceiverActivity.isSharedTextAnUrl(url));
}
}

View File

@ -4,7 +4,7 @@ buildscript {
google()
}
dependencies {
classpath "com.android.tools.build:gradle:4.2.2"
classpath "com.android.tools.build:gradle:8.1.2"
}
}
@ -15,7 +15,3 @@ allprojects {
maven { url "https://jitpack.io" }
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@ -15,10 +15,9 @@
org.gradle.jvmargs=-Xmx2048M
android.useAndroidX=true
minSdkVersion=21
targetSdkVersion=28
ndkVersion=22.1.7171670
compileSdkVersion=30
markwonVersion=4.6.2
minSdkVersion=24
compileSdkVersion=34
targetSdkVersion=34
ndkVersion=26.1.10909125
android.defaults.buildfeatures.buildconfig=true

Binary file not shown.

View File

@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

294
gradlew vendored
View File

@ -1,7 +1,7 @@
#!/usr/bin/env sh
#!/bin/sh
#
# Copyright 2015 the original author or authors.
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -17,67 +17,99 @@
#
##############################################################################
##
## Gradle start up script for UN*X
##
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
MAX_FD=maximum
warn () {
echo "$*"
}
} >&2
die () {
echo
echo "$*"
echo
exit 1
}
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@ -87,9 +119,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@ -98,88 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

15
gradlew.bat vendored
View File

@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@ -25,7 +25,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal

View File

@ -1 +1 @@
include ':app', ':termux-shared', ':terminal-emulator', ':terminal-view'
include ':app', ':terminal-emulator', ':terminal-view'

View File

@ -2,6 +2,8 @@ apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
android {
namespace "com.termux.emulator"
compileSdkVersion project.properties.compileSdkVersion.toInteger()
ndkVersion = System.getenv("JITPACK_NDK_VERSION") ?: project.properties.ndkVersion
@ -50,13 +52,13 @@ tasks.withType(Test) {
}
dependencies {
implementation "androidx.annotation:annotation:1.3.0"
implementation "androidx.annotation:annotation:1.7.0"
testImplementation "junit:junit:4.13.2"
}
task sourceJar(type: Jar) {
from android.sourceSets.main.java.srcDirs
classifier "sources"
archiveClassifier = "sources"
}
afterEvaluate {
@ -64,7 +66,7 @@ afterEvaluate {
publications {
// Creates a Maven publication called "release".
release(MavenPublication) {
from components.release
from components.findByName('release')
groupId = 'com.termux'
artifactId = 'terminal-emulator'
version = '0.118.0'

View File

@ -1,2 +1,2 @@
<manifest package="com.termux.terminal">
<manifest>
</manifest>

View File

@ -8,43 +8,8 @@ import java.io.StringWriter;
public class Logger {
public static void logError(TerminalSessionClient client, String logTag, String message) {
if (client != null)
client.logError(logTag, message);
else
Log.e(logTag, message);
}
public static void logWarn(TerminalSessionClient client, String logTag, String message) {
if (client != null)
client.logWarn(logTag, message);
else
Log.w(logTag, message);
}
public static void logInfo(TerminalSessionClient client, String logTag, String message) {
if (client != null)
client.logInfo(logTag, message);
else
Log.i(logTag, message);
}
public static void logDebug(TerminalSessionClient client, String logTag, String message) {
if (client != null)
client.logDebug(logTag, message);
else
Log.d(logTag, message);
}
public static void logVerbose(TerminalSessionClient client, String logTag, String message) {
if (client != null)
client.logVerbose(logTag, message);
else
Log.v(logTag, message);
}
public static void logStackTraceWithMessage(TerminalSessionClient client, String tag, String message, Throwable throwable) {
logError(client, tag, getMessageAndStackTraceString(message, throwable));
Log.e(tag, getMessageAndStackTraceString(message, throwable));
}
public static String getMessageAndStackTraceString(String message, Throwable throwable) {

View File

@ -1,6 +1,7 @@
package com.termux.terminal;
import android.util.Base64;
import android.util.Log;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
@ -28,65 +29,115 @@ import java.util.Stack;
*/
public final class TerminalEmulator {
/** Log unknown or unimplemented escape sequences received from the shell process. */
/**
* Log unknown or unimplemented escape sequences received from the shell process.
*/
private static final boolean LOG_ESCAPE_SEQUENCES = false;
public static final int MOUSE_LEFT_BUTTON = 0;
/** Mouse moving while having left mouse button pressed. */
/**
* Mouse moving while having left mouse button pressed.
*/
public static final int MOUSE_LEFT_BUTTON_MOVED = 32;
public static final int MOUSE_WHEELUP_BUTTON = 64;
public static final int MOUSE_WHEELDOWN_BUTTON = 65;
/** Used for invalid data - http://en.wikipedia.org/wiki/Replacement_character#Replacement_character */
/**
* Used for invalid data - http://en.wikipedia.org/wiki/Replacement_character#Replacement_character
*/
public static final int UNICODE_REPLACEMENT_CHAR = 0xFFFD;
/** Escape processing: Not currently in an escape sequence. */
/**
* Escape processing: Not currently in an escape sequence.
*/
private static final int ESC_NONE = 0;
/** Escape processing: Have seen an ESC character - proceed to {@link #doEsc(int)} */
/**
* Escape processing: Have seen an ESC character - proceed to {@link #doEsc(int)}
*/
private static final int ESC = 1;
/** Escape processing: Have seen ESC POUND */
/**
* Escape processing: Have seen ESC POUND
*/
private static final int ESC_POUND = 2;
/** Escape processing: Have seen ESC and a character-set-select ( char */
/**
* Escape processing: Have seen ESC and a character-set-select ( char
*/
private static final int ESC_SELECT_LEFT_PAREN = 3;
/** Escape processing: Have seen ESC and a character-set-select ) char */
/**
* Escape processing: Have seen ESC and a character-set-select ) char
*/
private static final int ESC_SELECT_RIGHT_PAREN = 4;
/** Escape processing: "ESC [" or CSI (Control Sequence Introducer). */
/**
* Escape processing: "ESC [" or CSI (Control Sequence Introducer).
*/
private static final int ESC_CSI = 6;
/** Escape processing: ESC [ ? */
/**
* Escape processing: ESC [ ?
*/
private static final int ESC_CSI_QUESTIONMARK = 7;
/** Escape processing: ESC [ $ */
/**
* Escape processing: ESC [ $
*/
private static final int ESC_CSI_DOLLAR = 8;
/** Escape processing: ESC % */
/**
* Escape processing: ESC %
*/
private static final int ESC_PERCENT = 9;
/** Escape processing: ESC ] (AKA OSC - Operating System Controls) */
/**
* Escape processing: ESC ] (AKA OSC - Operating System Controls)
*/
private static final int ESC_OSC = 10;
/** Escape processing: ESC ] (AKA OSC - Operating System Controls) ESC */
/**
* Escape processing: ESC ] (AKA OSC - Operating System Controls) ESC
*/
private static final int ESC_OSC_ESC = 11;
/** Escape processing: ESC [ > */
/**
* Escape processing: ESC [ >
*/
private static final int ESC_CSI_BIGGERTHAN = 12;
/** Escape procession: "ESC P" or Device Control String (DCS) */
/**
* Escape procession: "ESC P" or Device Control String (DCS)
*/
private static final int ESC_P = 13;
/** Escape processing: CSI > */
/**
* Escape processing: CSI >
*/
private static final int ESC_CSI_QUESTIONMARK_ARG_DOLLAR = 14;
/** Escape processing: CSI $ARGS ' ' */
/**
* Escape processing: CSI $ARGS ' '
*/
private static final int ESC_CSI_ARGS_SPACE = 15;
/** Escape processing: CSI $ARGS '*' */
/**
* Escape processing: CSI $ARGS '*'
*/
private static final int ESC_CSI_ARGS_ASTERIX = 16;
/** Escape processing: CSI " */
/**
* Escape processing: CSI "
*/
private static final int ESC_CSI_DOUBLE_QUOTE = 17;
/** Escape processing: CSI ' */
/**
* Escape processing: CSI '
*/
private static final int ESC_CSI_SINGLE_QUOTE = 18;
/** Escape processing: CSI ! */
/**
* Escape processing: CSI !
*/
private static final int ESC_CSI_EXCLAMATION = 19;
/** The number of parameter arguments. This name comes from the ANSI standard for terminal escape codes. */
/**
* The number of parameter arguments. This name comes from the ANSI standard for terminal escape codes.
*/
private static final int MAX_ESCAPE_PARAMETERS = 16;
/** Needs to be large enough to contain reasonable OSC 52 pastes. */
/**
* Needs to be large enough to contain reasonable OSC 52 pastes.
*/
private static final int MAX_OSC_STRING_LENGTH = 8192;
/** DECSET 1 - application cursor keys. */
/**
* DECSET 1 - application cursor keys.
*/
private static final int DECSET_BIT_APPLICATION_CURSOR_KEYS = 1;
private static final int DECSET_BIT_REVERSE_VIDEO = 1 << 1;
/**
@ -104,40 +155,66 @@ public final class TerminalEmulator {
* characters received when the cursor is at the right border of the page replace characters already on the page."
*/
private static final int DECSET_BIT_AUTOWRAP = 1 << 3;
/** DECSET 25 - if the cursor should be enabled, {@link #isCursorEnabled()}. */
/**
* DECSET 25 - if the cursor should be enabled, {@link #isCursorEnabled()}.
*/
private static final int DECSET_BIT_CURSOR_ENABLED = 1 << 4;
private static final int DECSET_BIT_APPLICATION_KEYPAD = 1 << 5;
/** DECSET 1000 - if to report mouse press&release events. */
/**
* DECSET 1000 - if to report mouse press&release events.
*/
private static final int DECSET_BIT_MOUSE_TRACKING_PRESS_RELEASE = 1 << 6;
/** DECSET 1002 - like 1000, but report moving mouse while pressed. */
/**
* DECSET 1002 - like 1000, but report moving mouse while pressed.
*/
private static final int DECSET_BIT_MOUSE_TRACKING_BUTTON_EVENT = 1 << 7;
/** DECSET 1004 - NOT implemented. */
/**
* DECSET 1004 - NOT implemented.
*/
private static final int DECSET_BIT_SEND_FOCUS_EVENTS = 1 << 8;
/** DECSET 1006 - SGR-like mouse protocol (the modern sane choice). */
/**
* DECSET 1006 - SGR-like mouse protocol (the modern sane choice).
*/
private static final int DECSET_BIT_MOUSE_PROTOCOL_SGR = 1 << 9;
/** DECSET 2004 - see {@link #paste(String)} */
/**
* DECSET 2004 - see {@link #paste(String)}
*/
private static final int DECSET_BIT_BRACKETED_PASTE_MODE = 1 << 10;
/** Toggled with DECLRMM - http://www.vt100.net/docs/vt510-rm/DECLRMM */
/**
* Toggled with DECLRMM - http://www.vt100.net/docs/vt510-rm/DECLRMM
*/
private static final int DECSET_BIT_LEFTRIGHT_MARGIN_MODE = 1 << 11;
/** Not really DECSET bit... - http://www.vt100.net/docs/vt510-rm/DECSACE */
/**
* Not really DECSET bit... - http://www.vt100.net/docs/vt510-rm/DECSACE
*/
private static final int DECSET_BIT_RECTANGULAR_CHANGEATTRIBUTE = 1 << 12;
private String mTitle;
private final Stack<String> mTitleStack = new Stack<>();
/** If processing first character of first parameter of {@link #ESC_CSI}. */
/**
* If processing first character of first parameter of {@link #ESC_CSI}.
*/
private boolean mIsCSIStart;
/** The last character processed of a parameter of {@link #ESC_CSI}. */
/**
* The last character processed of a parameter of {@link #ESC_CSI}.
*/
private Integer mLastCSIArg;
/** The cursor position. Between (0,0) and (mRows-1, mColumns-1). */
/**
* The cursor position. Between (0,0) and (mRows-1, mColumns-1).
*/
private int mCursorRow, mCursorCol;
/** The number of character rows and columns in the terminal screen. */
/**
* The number of character rows and columns in the terminal screen.
*/
public int mRows, mColumns;
/** The number of terminal transcript rows that can be scrolled back to. */
/**
* The number of terminal transcript rows that can be scrolled back to.
*/
public static final int TERMINAL_TRANSCRIPT_ROWS_MIN = 100;
public static final int TERMINAL_TRANSCRIPT_ROWS_MAX = 50000;
public static final int DEFAULT_TERMINAL_TRANSCRIPT_ROWS = 2000;
@ -151,11 +228,15 @@ public final class TerminalEmulator {
public static final int DEFAULT_TERMINAL_CURSOR_STYLE = TERMINAL_CURSOR_STYLE_BLOCK;
public static final Integer[] TERMINAL_CURSOR_STYLES_LIST = new Integer[]{TERMINAL_CURSOR_STYLE_BLOCK, TERMINAL_CURSOR_STYLE_UNDERLINE, TERMINAL_CURSOR_STYLE_BAR};
/** The terminal cursor styles. */
/**
* The terminal cursor styles.
*/
private int mCursorStyle = DEFAULT_TERMINAL_CURSOR_STYLE;
/** The normal screen buffer. Stores the characters that appear on the screen of the emulated terminal. */
/**
* The normal screen buffer. Stores the characters that appear on the screen of the emulated terminal.
*/
private final TerminalBuffer mMainBuffer;
/**
* The alternate screen buffer, exactly as large as the display and contains no additional saved lines (so that when
@ -164,20 +245,30 @@ public final class TerminalEmulator {
* See http://www.xfree86.org/current/ctlseqs.html#The%20Alternate%20Screen%20Buffer
*/
final TerminalBuffer mAltBuffer;
/** The current screen buffer, pointing at either {@link #mMainBuffer} or {@link #mAltBuffer}. */
/**
* The current screen buffer, pointing at either {@link #mMainBuffer} or {@link #mAltBuffer}.
*/
private TerminalBuffer mScreen;
/** The terminal session this emulator is bound to. */
/**
* The terminal session this emulator is bound to.
*/
private final TerminalOutput mSession;
TerminalSessionClient mClient;
/** Keeps track of the current argument of the current escape sequence. Ranges from 0 to MAX_ESCAPE_PARAMETERS-1. */
/**
* Keeps track of the current argument of the current escape sequence. Ranges from 0 to MAX_ESCAPE_PARAMETERS-1.
*/
private int mArgIndex;
/** Holds the arguments of the current escape sequence. */
/**
* Holds the arguments of the current escape sequence.
*/
private final int[] mArgs = new int[MAX_ESCAPE_PARAMETERS];
/** Holds OSC and device control arguments, which can be strings. */
/**
* Holds OSC and device control arguments, which can be strings.
*/
private final StringBuilder mOSCOrDeviceControlArgs = new StringBuilder();
/**
@ -186,13 +277,17 @@ public final class TerminalEmulator {
*/
private boolean mContinueSequence;
/** The current state of the escape sequence state machine. One of the ESC_* constants. */
/**
* The current state of the escape sequence state machine. One of the ESC_* constants.
*/
private int mEscapeState;
private final SavedScreenState mSavedStateMain = new SavedScreenState();
private final SavedScreenState mSavedStateAlt = new SavedScreenState();
/** http://www.vt100.net/docs/vt102-ug/table5-15.html */
/**
* http://www.vt100.net/docs/vt102-ug/table5-15.html
*/
private boolean mUseLineDrawingG0, mUseLineDrawingG1, mUseLineDrawingUsesG0 = true;
/**
@ -206,7 +301,9 @@ public final class TerminalEmulator {
*/
private boolean mInsertMode;
/** An array of tab stops. mTabStop[i] is true if there is a tab stop set for column i. */
/**
* An array of tab stops. mTabStop[i] is true if there is a tab stop set for column i.
*/
private boolean[] mTabStop;
/**
@ -243,7 +340,9 @@ public final class TerminalEmulator {
*/
int mForeColor, mBackColor;
/** Current {@link TextStyle} effect. */
/**
* Current {@link TextStyle} effect.
*/
private int mEffect;
/**
@ -252,7 +351,9 @@ public final class TerminalEmulator {
*/
private int mScrollCounter = 0;
/** If automatic scrolling of terminal is disabled */
/**
* If automatic scrolling of terminal is disabled
*/
private boolean mAutoScrollDisabled;
private byte mUtf8ToFollow, mUtf8Index;
@ -414,22 +515,18 @@ public final class TerminalEmulator {
return mCursorCol;
}
/** Get the terminal cursor style. It will be one of {@link #TERMINAL_CURSOR_STYLES_LIST} */
/**
* Get the terminal cursor style. It will be one of {@link #TERMINAL_CURSOR_STYLES_LIST}
*/
public int getCursorStyle() {
return mCursorStyle;
}
/** Set the terminal cursor style. */
/**
* Set the terminal cursor style.
*/
public void setCursorStyle() {
Integer cursorStyle = null;
if (mClient != null)
cursorStyle = mClient.getTerminalCursorStyle();
if (cursorStyle == null || !Arrays.asList(TERMINAL_CURSOR_STYLES_LIST).contains(cursorStyle))
mCursorStyle = DEFAULT_TERMINAL_CURSOR_STYLE;
else
mCursorStyle = cursorStyle;
mCursorStyle = DEFAULT_TERMINAL_CURSOR_STYLE;
}
public boolean isReverseVideo() {
@ -437,10 +534,10 @@ public final class TerminalEmulator {
}
public boolean isCursorEnabled() {
return isDecsetInternalBitSet(DECSET_BIT_CURSOR_ENABLED);
}
public boolean shouldCursorBeVisible() {
if (!isCursorEnabled())
return false;
@ -457,7 +554,6 @@ public final class TerminalEmulator {
}
public boolean isKeypadApplicationMode() {
return isDecsetInternalBitSet(DECSET_BIT_APPLICATION_KEYPAD);
}
@ -466,7 +562,9 @@ public final class TerminalEmulator {
return isDecsetInternalBitSet(DECSET_BIT_APPLICATION_CURSOR_KEYS);
}
/** If mouse events are being sent as escape codes to the terminal. */
/**
* If mouse events are being sent as escape codes to the terminal.
*/
public boolean isMouseTrackingActive() {
return isDecsetInternalBitSet(DECSET_BIT_MOUSE_TRACKING_PRESS_RELEASE) || isDecsetInternalBitSet(DECSET_BIT_MOUSE_TRACKING_BUTTON_EVENT);
}
@ -831,7 +929,7 @@ public final class TerminalEmulator {
if (internalBit != -1) {
value = isDecsetInternalBitSet(internalBit) ? 1 : 2; // 1=set, 2=reset.
} else {
Logger.logError(mClient, LOG_TAG, "Got DECRQM for unrecognized private DEC mode=" + mode);
Log.e(LOG_TAG, "Got DECRQM for unrecognized private DEC mode=" + mode);
value = 0; // 0=not recognized, 3=permanently set, 4=permanently reset
}
}
@ -886,7 +984,9 @@ public final class TerminalEmulator {
}
}
/** When in {@link #ESC_P} ("device control") sequence. */
/**
* When in {@link #ESC_P} ("device control") sequence.
*/
private void doDeviceControl(int b) {
switch (b) {
case (byte) '\\': // End of ESC \ string Terminator
@ -975,7 +1075,7 @@ public final class TerminalEmulator {
case "&8": // Undo key - ignore.
break;
default:
Logger.logWarn(mClient, LOG_TAG, "Unhandled termcap/terminfo name: '" + trans + "'");
Log.w(LOG_TAG, "Unhandled termcap/terminfo name: '" + trans + "'");
}
// Respond with invalid request:
mSession.write("\033P0+r" + part + "\033\\");
@ -987,12 +1087,12 @@ public final class TerminalEmulator {
mSession.write("\033P1+r" + part + "=" + hexEncoded + "\033\\");
}
} else {
Logger.logError(mClient, LOG_TAG, "Invalid device termcap/terminfo name of odd length: " + part);
Log.e(LOG_TAG, "Invalid device termcap/terminfo name of odd length: " + part);
}
}
} else {
if (LOG_ESCAPE_SEQUENCES)
Logger.logError(mClient, LOG_TAG, "Unrecognized device control string: " + dcs);
Log.e(LOG_TAG, "Unrecognized device control string: " + dcs);
}
finishSequence();
}
@ -1015,7 +1115,9 @@ public final class TerminalEmulator {
return mRightMargin - 1;
}
/** Process byte while in the {@link #ESC_CSI_QUESTIONMARK} escape state. */
/**
* Process byte while in the {@link #ESC_CSI_QUESTIONMARK} escape state.
*/
private void doCsiQuestionMark(int b) {
switch (b) {
case 'J': // Selective erase in display (DECSED) - http://www.vt100.net/docs/vt510-rm/DECSED.
@ -1082,7 +1184,7 @@ public final class TerminalEmulator {
int externalBit = mArgs[i];
int internalBit = mapDecSetBitToInternalBit(externalBit);
if (internalBit == -1) {
Logger.logWarn(mClient, LOG_TAG, "Ignoring request to save/recall decset bit=" + externalBit);
Log.w(LOG_TAG, "Ignoring request to save/recall decset bit=" + externalBit);
} else {
if (b == 's') {
mSavedDecSetFlags |= internalBit;
@ -1272,7 +1374,7 @@ public final class TerminalEmulator {
// (1) enables this feature for keys except for those with well-known behavior, e.g., Tab, Backarrow and
// some special control character cases, e.g., Control-Space to make a NUL.
// (2) enables this feature for keys including the exceptions listed.
Logger.logError(mClient, LOG_TAG, "(ignored) CSI > MODIFY RESOURCE: " + getArg0(-1) + " to " + getArg1(-1));
Log.e(LOG_TAG, "(ignored) CSI > MODIFY RESOURCE: " + getArg0(-1) + " to " + getArg1(-1));
break;
default:
parseArg(b);
@ -1319,7 +1421,9 @@ public final class TerminalEmulator {
}
}
/** Encountering a character in the {@link #ESC} state. */
/**
* Encountering a character in the {@link #ESC} state.
*/
private void doEsc(int b) {
switch (b) {
case '#':
@ -1412,7 +1516,9 @@ public final class TerminalEmulator {
}
}
/** DECSC save cursor - http://www.vt100.net/docs/vt510-rm/DECSC . See {@link #restoreCursor()}. */
/**
* DECSC save cursor - http://www.vt100.net/docs/vt510-rm/DECSC . See {@link #restoreCursor()}.
*/
private void saveCursor() {
SavedScreenState state = (mScreen == mMainBuffer) ? mSavedStateMain : mSavedStateAlt;
state.mSavedCursorRow = mCursorRow;
@ -1426,7 +1532,9 @@ public final class TerminalEmulator {
state.mUseLineDrawingUsesG0 = mUseLineDrawingUsesG0;
}
/** DECRS restore cursor - http://www.vt100.net/docs/vt510-rm/DECRC. See {@link #saveCursor()}. */
/**
* DECRS restore cursor - http://www.vt100.net/docs/vt510-rm/DECRC. See {@link #saveCursor()}.
*/
private void restoreCursor() {
SavedScreenState state = (mScreen == mMainBuffer) ? mSavedStateMain : mSavedStateAlt;
setCursorRowCol(state.mSavedCursorRow, state.mSavedCursorCol);
@ -1440,7 +1548,9 @@ public final class TerminalEmulator {
mUseLineDrawingUsesG0 = state.mUseLineDrawingUsesG0;
}
/** Following a CSI - Control Sequence Introducer, "\033[". {@link #ESC_CSI}. */
/**
* Following a CSI - Control Sequence Introducer, "\033[". {@link #ESC_CSI}.
*/
private void doCsi(int b) {
switch (b) {
case '!':
@ -1761,7 +1871,9 @@ public final class TerminalEmulator {
}
}
/** Select Graphic Rendition (SGR) - see http://en.wikipedia.org/wiki/ANSI_escape_code#graphics. */
/**
* Select Graphic Rendition (SGR) - see http://en.wikipedia.org/wiki/ANSI_escape_code#graphics.
*/
private void selectGraphicRendition() {
if (mArgIndex >= mArgs.length) mArgIndex = mArgs.length - 1;
for (int i = 0; i <= mArgIndex; i++) {
@ -1821,7 +1933,7 @@ public final class TerminalEmulator {
int firstArg = mArgs[i + 1];
if (firstArg == 2) {
if (i + 4 > mArgIndex) {
Logger.logWarn(mClient, LOG_TAG, "Too few CSI" + code + ";2 RGB arguments");
Log.w(LOG_TAG, "Too few CSI" + code + ";2 RGB arguments");
} else {
int red = mArgs[i + 2], green = mArgs[i + 3], blue = mArgs[i + 4];
if (red < 0 || green < 0 || blue < 0 || red > 255 || green > 255 || blue > 255) {
@ -1846,7 +1958,7 @@ public final class TerminalEmulator {
mBackColor = color;
}
} else {
if (LOG_ESCAPE_SEQUENCES) Logger.logWarn(mClient, LOG_TAG, "Invalid color index: " + color);
if (LOG_ESCAPE_SEQUENCES) Log.w(LOG_TAG, "Invalid color index: " + color);
}
} else {
finishSequenceAndLogError("Invalid ISO-8613-3 SGR first argument: " + firstArg);
@ -1863,7 +1975,7 @@ public final class TerminalEmulator {
mBackColor = code - 100 + 8;
} else {
if (LOG_ESCAPE_SEQUENCES)
Logger.logWarn(mClient, LOG_TAG, String.format("SGR unknown code %d", code));
Log.w(LOG_TAG, String.format("SGR unknown code %d", code));
}
}
}
@ -1897,7 +2009,9 @@ public final class TerminalEmulator {
}
}
/** An Operating System Controls (OSC) Set Text Parameters. May come here from BEL or ST. */
/**
* An Operating System Controls (OSC) Set Text Parameters. May come here from BEL or ST.
*/
private void doOscSetTextParameters(String bellOrStringTerminator) {
int value = -1;
String textParameter = "";
@ -1997,7 +2111,7 @@ public final class TerminalEmulator {
String clipboardText = new String(Base64.decode(textParameter.substring(startIndex), 0), StandardCharsets.UTF_8);
mSession.onCopyTextToClipboard(clipboardText);
} catch (Exception e) {
Logger.logError(mClient, LOG_TAG, "OSC Manipulate selection, invalid string '" + textParameter + "");
Log.e(LOG_TAG, "OSC Manipulate selection, invalid string '" + textParameter + "");
}
break;
case 104:
@ -2054,7 +2168,9 @@ public final class TerminalEmulator {
return TextStyle.encode(mForeColor, mBackColor, mEffect);
}
/** "CSI P_m h" for set or "CSI P_m l" for reset ANSI mode. */
/**
* "CSI P_m h" for set or "CSI P_m l" for reset ANSI mode.
*/
private void doSetMode(boolean newValue) {
int modeBit = getArg0(0);
switch (modeBit) {
@ -2104,7 +2220,7 @@ public final class TerminalEmulator {
/**
* Process the next ASCII character of a parameter.
*
* <p>
* Parameter characters modify the action or interpretation of the sequence. You can use up to
* 16 parameters per sequence. You must use the ; character to separate parameters.
* All parameters are unsigned, positive decimal integers, with the most significant
@ -2112,16 +2228,16 @@ public final class TerminalEmulator {
* (decimal). If you do not specify a value, a 0 value is assumed. A 0 value
* or omitted parameter indicates a default value for the sequence. For most
* sequences, the default value is 1.
*
* <p>
* https://vt100.net/docs/vt510-rm/chapter4.html#S4.3.3
* */
*/
private void parseArg(int inputByte) {
int[] bytes = new int[]{inputByte};
// Only doing this for ESC_CSI and not for other ESC_CSI_* since they seem to be using their
// own defaults with getArg*() calls, but there may be missed cases
if (mEscapeState == ESC_CSI) {
if ((mIsCSIStart && inputByte == ';') || // If sequence starts with a ; character, like \033[;m
(!mIsCSIStart && mLastCSIArg != null && mLastCSIArg == ';' && inputByte == ';')) { // If sequence contains sequential ; characters, like \033[;;m
(!mIsCSIStart && mLastCSIArg != null && mLastCSIArg == ';' && inputByte == ';')) { // If sequence contains sequential ; characters, like \033[;;m
bytes = new int[]{'0', ';'}; // Assume 0 was passed
}
}
@ -2222,7 +2338,7 @@ public final class TerminalEmulator {
}
private void finishSequenceAndLogError(String error) {
if (LOG_ESCAPE_SEQUENCES) Logger.logWarn(mClient, LOG_TAG, error);
if (LOG_ESCAPE_SEQUENCES) Log.w(LOG_TAG, error);
finishSequence();
}
@ -2395,12 +2511,16 @@ public final class TerminalEmulator {
mAboutToAutoWrap = false;
}
/** Set the cursor mode, but limit it to margins if {@link #DECSET_BIT_ORIGIN_MODE} is enabled. */
/**
* Set the cursor mode, but limit it to margins if {@link #DECSET_BIT_ORIGIN_MODE} is enabled.
*/
private void setCursorColRespectingOriginMode(int col) {
setCursorPosition(col, mCursorRow);
}
/** TODO: Better name, distinguished from {@link #setCursorPosition(int, int)} by not regarding origin mode. */
/**
* TODO: Better name, distinguished from {@link #setCursorPosition(int, int)} by not regarding origin mode.
*/
private void setCursorRowCol(int row, int col) {
mCursorRow = Math.max(0, Math.min(row, mRows - 1));
mCursorCol = Math.max(0, Math.min(col, mColumns - 1));
@ -2424,7 +2544,9 @@ public final class TerminalEmulator {
}
/** Reset terminal state so user can interact with it regardless of present state. */
/**
* Reset terminal state so user can interact with it regardless of present state.
*/
public void reset() {
setCursorStyle();
mArgIndex = 0;
@ -2461,12 +2583,16 @@ public final class TerminalEmulator {
return mScreen.getSelectedText(x1, y1, x2, y2);
}
/** Get the terminal session's title (null if not set). */
/**
* Get the terminal session's title (null if not set).
*/
public String getTitle() {
return mTitle;
}
/** Change the terminal session's title. */
/**
* Change the terminal session's title.
*/
private void setTitle(String newTitle) {
String oldTitle = mTitle;
mTitle = newTitle;
@ -2475,7 +2601,9 @@ public final class TerminalEmulator {
}
}
/** If DECSET 2004 is set, prefix paste with "\033[200~" and suffix with "\033[201~". */
/**
* If DECSET 2004 is set, prefix paste with "\033[200~" and suffix with "\033[201~".
*/
public void paste(String text) {
// First: Always remove escape key and C1 control characters [0x80,0x9F]:
text = text.replaceAll("(\u001B|[\u0080-\u009F])", "");
@ -2489,9 +2617,13 @@ public final class TerminalEmulator {
if (bracketed) mSession.write("\033[201~");
}
/** http://www.vt100.net/docs/vt510-rm/DECSC */
/**
* http://www.vt100.net/docs/vt510-rm/DECSC
*/
static final class SavedScreenState {
/** Saved state of the cursor position, Used to implement the save/restore cursor position escape sequences. */
/**
* Saved state of the cursor position, Used to implement the save/restore cursor position escape sequences.
*/
int mSavedCursorRow, mSavedCursorCol;
int mSavedEffect, mSavedForeColor, mSavedBackColor;
int mSavedDecFlags;

View File

@ -6,6 +6,7 @@ import android.os.Message;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import android.util.Log;
import java.io.File;
import java.io.FileDescriptor;
@ -88,17 +89,6 @@ public final class TerminalSession extends TerminalOutput {
this.mClient = client;
}
/**
* @param client The {@link TerminalSessionClient} interface implementation to allow
* for communication between {@link TerminalSession} and its client.
*/
public void updateTerminalSessionClient(TerminalSessionClient client) {
mClient = client;
if (mEmulator != null)
mEmulator.updateTerminalSessionClient(client);
}
/** Inform the attached pty of the new size and reflow or initialize the emulator. */
public void updateSize(int columns, int rows) {
if (mEmulator == null) {
@ -126,7 +116,6 @@ public final class TerminalSession extends TerminalOutput {
int[] processId = new int[1];
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);
mShellPid = processId[0];
mClient.setTerminalShellPid(this, mShellPid);
final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor, mClient);
@ -237,7 +226,7 @@ public final class TerminalSession extends TerminalOutput {
try {
Os.kill(mShellPid, OsConstants.SIGKILL);
} catch (ErrnoException e) {
Logger.logWarn(mClient, LOG_TAG, "Failed sending SIGKILL: " + e.getMessage());
Log.w(LOG_TAG, "Failed sending SIGKILL: " + e.getMessage());
}
}
}

View File

@ -26,26 +26,4 @@ public interface TerminalSessionClient {
void onTerminalCursorStateChange(boolean state);
void setTerminalShellPid(@NonNull TerminalSession session, int pid);
Integer getTerminalCursorStyle();
void logError(String tag, String message);
void logWarn(String tag, String message);
void logInfo(String tag, String message);
void logDebug(String tag, String message);
void logVerbose(String tag, String message);
void logStackTraceWithMessage(String tag, String message, Exception e);
void logStackTrace(String tag, Exception e);
}

View File

@ -2,10 +2,11 @@ apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
android {
namespace "com.termux.view"
compileSdkVersion project.properties.compileSdkVersion.toInteger()
dependencies {
implementation "androidx.annotation:annotation:1.3.0"
implementation "androidx.annotation:annotation:1.7.0"
api project(":terminal-emulator")
}
@ -34,7 +35,7 @@ dependencies {
task sourceJar(type: Jar) {
from android.sourceSets.main.java.srcDirs
classifier "sources"
archiveClassifier = "sources"
}
afterEvaluate {
@ -42,7 +43,7 @@ afterEvaluate {
publications {
// Creates a Maven publication called "release".
release(MavenPublication) {
from components.release
from components.findByName('release')
groupId = 'com.termux'
artifactId = 'terminal-view'
version = '0.118.0'
@ -51,3 +52,4 @@ afterEvaluate {
}
}
}

View File

@ -1,2 +1,2 @@
<manifest package="com.termux.view">
<manifest>
</manifest>

View File

@ -5,7 +5,9 @@ import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
/** A combination of {@link GestureDetector} and {@link ScaleGestureDetector}. */
/**
* A combination of {@link GestureDetector} and {@link ScaleGestureDetector}.
*/
final class GestureAndScaleRecognizer {
public interface Listener {

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