Compare commits

...
69 Commits
Author SHA1 Message Date
Fredrik Fornwall 9b7c7102b2 Bump version to 0.94
Build / build (push) Canceled after 0s
Unit tests / testing (push) Canceled after 0s
2020-03-24 23:25:49 +01:00
Daniel Lublin 1819087ca0 Add tests for URL fragment 2020-03-24 23:13:14 +01:00
Daniel Lublin 366a61f052 Grab fragment when extracting URLs for selection 2020-03-24 23:13:14 +01:00
Leonid Plyushch 6e224cabcf set ndk version in build.gradle
Fixing CI builds.
2020-03-21 11:31:52 +02:00
Leonid Plyushch 8c8fa96133 update bootstraps 2020-03-21 10:35:17 +02:00
Fredrik Fornwall 537f2ed97e Update gradle wrapper 2020-03-20 14:42:43 +01:00
Fredrik Fornwall 0e23315c41 Add android.useAndroidX=true to gradle.properties 2020-03-20 10:44:24 +01:00
Fredrik Fornwall 2cde986419 Update Android gradle plug-in 2020-03-16 13:58:34 +01:00
Fredrik Fornwall d28939810c Update Robolectric from 4.3 to 4.3.1 2020-02-21 14:02:53 +01:00
Fredrik Fornwall 93724b7aa6 Bump version to 0.92
Build / build (push) Canceled after 0s
Unit tests / testing (push) Canceled after 0s
2020-02-09 22:59:33 +01:00
x0b 5d06f040e8 Fix deletable flags in DocumentProvider
A file is deletable if the _parent_ is writable, not the file itself.
2020-02-09 22:57:27 +01:00
x0b ed9afa082a Fix #1350: Support createDocument(...) 2020-02-09 22:57:27 +01:00
x0b 9703bd31ad Fix #1424: Support ACTION_OPEN_DOCUMENT_TREE 2020-02-09 22:57:27 +01:00
Leonid Plyushch 9749f25eba update bootstrap archives
v19 had a problem with metadata which broke the package manager.
2020-02-09 12:34:11 +02:00
Leonid Plyushch 453b838b24 update bootstrap archives 2020-02-08 20:33:34 +02:00
Leonid Plyushch 9fdf2a49fd update readme 2020-02-07 15:48:32 +02:00
Leonid Plyushch 3270506bff update issue templates 2020-02-07 15:05:16 +02:00
Leonid Plyushch a240f4cf45 CI: fix job name in run_tests.yml 2020-02-07 14:02:34 +02:00
Leonid Plyushch 4647beb0d2 update readme 2020-02-07 13:57:08 +02:00
Leonid Plyushch d92e806461 CI: update workflow labels 2020-02-07 13:55:45 +02:00
Leonid Plyushch b75cf0bb84 CI: add configuration for running terminal emulator unit tests 2020-02-07 13:50:12 +02:00
Leonid Plyushch f928efed4e CI: switch to Github Actions 2020-02-07 13:47:22 +02:00
Fredrik Fornwall 36db64d585 Bump version to 0.90 2020-02-02 03:36:52 +01:00
Jean Schurger b8f0430699 Prevent kill by Samsung Dex on plug/unplug. 2020-02-02 03:36:13 +01:00
Edontin 90e6260d5e Allow the user to disable virtual key emulation.
Use volume-keys=volume within termux.properties to disable.
2020-01-06 10:21:32 +01:00
Fredrik Fornwall 566d656c16 Avoid trailing slash in CWD (fixes #1413) 2020-01-05 19:14:52 +01:00
Fredrik Fornwall b729085d52 Bump version to 0.88 2020-01-05 19:14:35 +01:00
Fredrik Fornwall a94bcb02f1 Bump version to 0.86 2020-01-05 02:02:05 +01:00
Fredrik Fornwall e28be01dc2 Create new terminal sessions with directory of active session
This mimics the behaviour of most tabbed terminal emulators.

Fixes #1009.
2020-01-05 02:00:25 +01:00
Archenoth 7f7c1efac1 Add PendingIntent in TermuxService to return data from execution
This commit adds an optional final argument to the BackgroundJob
constructor for a PendingIntent to return the results of its
execution to, and also attempts to pass an optional pendingIntent to
it from the Service start intent
2020-01-05 01:07:11 +01:00
Archenoth 76df44e6bb Start stderr BackgroundJob Thread 2020-01-05 01:07:11 +01:00
Fredrik Fornwall fd13f3f98d Handle magnet links with termux-url-opener
Fixes #1339 and #1382.
2020-01-05 01:02:47 +01:00
Leonid Plyushch 269c3cafb0 properly set bootclasspath environment variable 2020-01-05 00:38:49 +01:00
Fredrik Fornwall 662b5cace6 Update gradle wrapper 2020-01-01 22:57:52 +01:00
Fredrik Fornwall 3c01b09fff Bump version to 0.85 2019-12-29 18:53:00 +01:00
Fredrik Fornwall 1ce2db9929 Merge branch 'maoabc-master' 2019-12-29 18:50:34 +01:00
Fredrik Fornwall c987ff718a Update Android Gradle plug-in 2019-12-29 18:49:41 +01:00
Leonid Plyushch 9e295b105c terminal emulator: clear scrollback buffer when resetting to initial state
Fixes utility 'reset' not being able to clear scrollback buffer.
2019-12-29 18:49:41 +01:00
Fredrik Fornwall 37b3a9f8db Update Android Gradle plug-in 2019-12-29 18:49:10 +01:00
Leonid Plyushch 490853e427 terminal emulator: clear scrollback buffer when resetting to initial state
Fixes utility 'reset' not being able to clear scrollback buffer.
2019-12-24 00:32:40 +01:00
mao 9ebedc4740 Merge remote-tracking branch 'remotes/origin/master' 2019-12-19 00:43:45 +08:00
mao 82730d9e08 confict 2019-12-19 00:34:14 +08:00
mao 3d2756f376 Optimize handleView move. 2019-12-18 23:50:23 +08:00
Leonid Plyushch 6db51bdec0 remove SDK_INT conditionals
We are targting on API 24, so conditionals for Android 5/6 compatibillity
are useless now.
2019-12-18 14:06:06 +02:00
mao c238c8bddb Optimize handle view 2019-12-18 14:00:58 +02:00
mao 60e1556871 Stop selection mode on enter 2019-12-18 14:00:57 +02:00
mao e47bd31681 Selection mode fling 2019-12-18 14:00:57 +02:00
mao 951df6b4e2 Add selection mode cursor controller 2019-12-18 14:00:32 +02:00
Leonid Plyushch fcd3bc1133 update gradle wrapper 2019-12-02 17:19:17 +02:00
Fredrik Fornwall f4db9ff212 Bump version to 0.84 2019-11-24 19:10:10 +01:00
Leonid Plyushch 87841886d4 extra keys: improve handling of DnD mode
Do not disturb mode is now handled only for SDKs pre-28. Extra keys will
not vibrate only when total silence mode is used.
2019-11-24 19:08:32 +01:00
Leonid Plyushch cf883f5f05 extra keys: handle actions UP & CANCEL separately
Related issue: https://github.com/termux/termux-app/issues/905
2019-11-24 19:08:32 +01:00
Fredrik Fornwall 677d75e173 Remove outdated tests
Remove tests that asserted that Cursor Down (CUD) and Cursor Up (CUU)
escape sequences were affected by the scrolling region set by DECSTBM.

This was incorrect and recently fixed:
https://github.com/termux/termux-app/issues/1340
2019-11-24 19:07:34 +01:00
Fredrik Fornwall cdccc2c433 Do not limit cursor movement to scroll region
The scrolling region set by DECSTBM should not affect the
Cursor Down (CUD) and Cursor Up (CUU) escape sequences.

Fixes #1340.
2019-11-10 22:05:06 +01:00
Fredrik Fornwall 070436a6ed Bump version to 0.82 2019-11-10 19:26:10 +01:00
Fredrik Fornwall 69ded4b33e Update Android gradle plug-in 2019-11-10 19:26:01 +01:00
Fredrik Fornwall e5a8c0eb4a Cleanup after requiring Android 7.0 2019-11-03 22:39:21 +01:00
Fredrik Fornwall 29815fb3f0 Fix gradle deprecation warning 2019-11-03 22:35:52 +01:00
Fredrik Fornwall e930ac08aa Bump version to 0.78 2019-11-03 19:18:31 +01:00
Fredrik Fornwall 3b969a0077 Bump version to 0.77 2019-10-28 01:54:50 +01:00
Leonid Plyushch ae717d8f5f do not allow to backup data by Google
Why:

 * During backup process Termux is being killed in most cases.
 * Backup data is limited to 25 MB.
 * Backup may not be performed/updated in certain cases.
2019-10-27 20:33:12 +01:00
Leonid Plyushch 6c55c3c1be bootstraps: update to v18 2019-10-20 22:06:18 +03:00
Fredrik Fornwall c0a0822843 Do not set LD_LIBRARY_PATH by default
See https://github.com/termux/termux-app/issues/1286
2019-10-20 20:48:10 +02:00
Fredrik Fornwall 08b08d1c47 Bump version to 0.76 2019-10-20 20:16:48 +02:00
Fredrik Fornwall c76cec186a Include bootstrap zips as shared libraries 2019-10-20 20:15:50 +02:00
mao fdb3764f5c Optimize handle view 2019-10-11 07:37:57 +08:00
mao 937eb350b2 Stop selection mode on enter 2019-10-05 18:54:15 +08:00
mao 3b4ece6bd8 Selection mode fling 2019-10-05 18:30:54 +08:00
mao 35a4fdacbe Add selection mode cursor controller 2019-10-05 18:05:42 +08:00
37 changed files with 1231 additions and 327 deletions
-20
View File
@@ -1,20 +0,0 @@
container:
image: cirrusci/android-sdk:28
cpu: 2
memory: 8G
task:
name: tests
script: ./gradlew test
task:
name: debug-build
depends_on:
- tests
script: |
./gradlew assembleDebug
output_artifacts:
path: "./app/build/outputs/apk/debug/*.apk"
+21 -6
View File
@@ -1,20 +1,35 @@
---
name: Bug report
about: Create a report to help us improve termux-app
about: Create a report to help us improve Termux application
---
<!-- Important note: Refusing to provide needed information may result in issue closing. If you are having problems with a package in termux then a bug report should be filed in the termux-packages repo: https://github.com/termux/termux-packages -->
<!--
IMPORTANT:
1. Support of Android 5.x - 6.x is finished.
2. Fill the template AFTER comments.
-->
**Problem description**
A clear and concise description of what the problem with the termux app is. You may post screenshots in addition to description.
<!--
A clear and concise description of what the problem is.
You may post screenshots in addition to description.
-->
**Steps to reproduce**
Please post all steps that are needed to reproduce the issue.
<!--
Steps to reproduce the behavior. Please post all necessary
commands that are needed to reproduce the issue.
-->
**Expected behavior**
<!--
A clear and concise description of what you expected to happen.
-->
**Additional information**
Post output of command `termux-info`.
If you are rooted or have access to adb then capture a logcat with `logcat -d "*:W"`, from a adb or root shell.
* Termux application version:
* Android OS version:
* Device model:
+12 -2
View File
@@ -1,12 +1,22 @@
---
name: Feature request
about: Suggest a new feature in termux-app
about: Suggest a new feature for Termux application
---
<!--
IMPORTANT:
1. Support of Android 5.x - 6.x is finished.
2. Fill the template AFTER comments.
-->
**Feature description**
<!--
Describe the feature and why you want it.
-->
**Reference implementation**
Does another app/terminal emulator have this feature?
Provide links to more background information
Provide links to more background information.
+18
View File
@@ -0,0 +1,18 @@
name: Build
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v2
- name: Build
run: |
./gradlew assembleDebug
- name: Store generated APK file
uses: actions/upload-artifact@v1
with:
name: termux-app
path: ./app/build/outputs/apk/debug/app-debug.apk
+13
View File
@@ -0,0 +1,13 @@
name: Unit tests
on: push
jobs:
testing:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v2
- name: Execute tests
run: |
./gradlew test
+1
View File
@@ -8,6 +8,7 @@ build/
*.so
.externalNativeBuild
.cxx
*.zip
# Crashlytics configuations
com_crashlytics_export_strings.xml
+6 -4
View File
@@ -1,6 +1,7 @@
# Termux application
[![Build status](https://api.cirrus-ci.com/github/termux/termux-app.svg?branch=master)](https://cirrus-ci.com/termux/termux-app)
[![Build status](https://github.com/termux/termux-app/workflows/Build/badge.svg)](https://github.com/termux/termux-app/actions)
[![Testing status](https://github.com/termux/termux-app/workflows/Unit%20tests/badge.svg)](https://github.com/termux/termux-app/actions)
[![Join the chat at https://gitter.im/termux/termux](https://badges.gitter.im/termux/termux.svg)](https://gitter.im/termux/termux)
[Termux](https://termux.com) is an Android terminal application and Linux environment.
@@ -21,9 +22,10 @@ Termux:Widget application can be obtained from:
- [F-Droid](https://f-droid.org/en/packages/com.termux/)
- [Kali Nethunter Store](https://store.nethunter.com/en/packages/com.termux/)
Additionally we offer development builds for those who want to try out latest
features ready to be included in future versions. Such build can be obtained
directly from [Cirrus CI artifacts](https://api.cirrus-ci.com/v1/artifact/github/termux/termux-app/debug-build/output/app/build/outputs/apk/debug/app-debug.apk).
Additionally we provide per-commit debug builds for those who want to try
out the latest features or test their pull request. This build can be obtained
from one of the workflow runs listed on [Github Actions](https://github.com/termux/termux-app/actions)
page.
Signature keys of all offered builds are different. Before you switch the
installation source, you will have to uninstall the Termux application and
+98 -6
View File
@@ -1,10 +1,13 @@
apply plugin: 'com.android.application'
plugins {
id "com.android.application"
}
android {
compileSdkVersion 28
ndkVersion '21.0.6113669'
dependencies {
implementation "androidx.annotation:annotation:1.0.1"
implementation "androidx.annotation:annotation:1.1.0"
implementation "androidx.viewpager:viewpager:1.0.0"
implementation "androidx.drawerlayout:drawerlayout:1.0.0"
implementation project(":terminal-view")
@@ -12,10 +15,21 @@ android {
defaultConfig {
applicationId "com.termux"
minSdkVersion 21
minSdkVersion 24
targetSdkVersion 28
versionCode 75
versionName "0.75"
versionCode 94
versionName "0.94"
externalNativeBuild {
ndkBuild {
cFlags "-std=c11", "-Wall", "-Wextra", "-Werror", "-Os", "-fno-stack-protector", "-Wl,--gc-sections"
}
}
ndk {
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
}
}
signingConfigs {
@@ -43,10 +57,23 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
externalNativeBuild {
ndkBuild {
path "src/main/cpp/Android.mk"
}
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
dependencies {
testImplementation 'junit:junit:4.12'
testImplementation 'junit:junit:4.13'
testImplementation 'org.robolectric:robolectric:4.3.1'
}
task versionName {
@@ -54,3 +81,68 @@ task versionName {
print android.defaultConfig.versionName
}
}
def downloadBootstrap(String arch, String expectedChecksum, int version) {
def digest = java.security.MessageDigest.getInstance("SHA-256")
def localUrl = "src/main/cpp/bootstrap-" + arch + ".zip"
def file = new File(projectDir, localUrl)
if (file.exists()) {
def buffer = new byte[8192]
def input = new FileInputStream(file)
while (true) {
def readBytes = input.read(buffer)
if (readBytes < 0) break
digest.update(buffer, 0, readBytes)
}
def checksum = new BigInteger(1, digest.digest()).toString(16)
if (checksum == expectedChecksum) {
return
} else {
logger.quiet("Deleting old local file with wrong hash: " + localUrl)
file.delete()
}
}
def remoteUrl = "https://bintray.com/termux/bootstrap/download_file?file_path=bootstrap-" + arch + "-v" + version + ".zip"
logger.quiet("Downloading " + remoteUrl + " ...")
file.parentFile.mkdirs()
def out = new BufferedOutputStream(new FileOutputStream(file))
def connection = new URL(remoteUrl).openConnection()
connection.setInstanceFollowRedirects(true)
def digestStream = new java.security.DigestInputStream(connection.inputStream, digest)
out << digestStream
out.close()
def checksum = new BigInteger(1, digest.digest()).toString(16)
if (checksum != expectedChecksum) {
file.delete()
throw new GradleException("Wrong checksum for " + remoteUrl + ": expected: " + expectedChecksum + ", actual: " + checksum)
}
}
clean {
doLast {
def tree = fileTree(new File(projectDir, 'src/main/cpp'))
tree.include 'bootstrap-*.zip'
tree.each { it.delete() }
}
}
task downloadBootstraps(){
doLast {
def version = 21
downloadBootstrap("aarch64", "9d362f1396e3a7d6987f09b8c3886c5aad514ab195ec7874a32000dc3b9cecde", version)
downloadBootstrap("arm", "3c2f2ca9424fc984a4df54a3f9b38db3413b635ba39b89dfd8c753418730f719", version)
downloadBootstrap("i686", "87afbd0fe6ebdc3abd9f2e5bc6136dbbf49991e485c9e9b03c4fe0149543ed7d", version)
downloadBootstrap("x86_64", "bc91e5524d516cbe34b2ef082f6ebda1146457c8fa0373d06f7a1bb0c7be41df", version)
}
}
afterEvaluate {
android.applicationVariants.all { variant ->
variant.javaCompileProvider.get().dependsOn(downloadBootstraps)
}
}
+7 -8
View File
@@ -14,12 +14,11 @@
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission-sdk-23 android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<application
android:extractNativeLibs="true"
android:allowBackup="true"
android:fullBackupContent="@xml/backupscheme"
android:allowBackup="false"
android:icon="@drawable/ic_launcher"
android:banner="@drawable/banner"
android:label="@string/application_name"
@@ -32,11 +31,11 @@
<activity
android:name="com.termux.app.TermuxActivity"
android:label="@string/application_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:label="@string/application_name"
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|uiMode|keyboard|keyboardHidden|navigation"
android:launchMode="singleTask"
android:resizeableActivity="true"
android:windowSoftInputMode="adjustResize|stateAlwaysVisible" >
android:windowSoftInputMode="adjustResize|stateAlwaysVisible" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -121,8 +120,8 @@
android:exported="true"
android:grantUriPermissions="true"
android:name="com.termux.app.TermuxOpenReceiver$ContentProvider" />
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
<meta-data android:name="com.samsung.android.multidisplay.keep_process_alive" android:value="true"/>
</application>
</manifest>
+5
View File
@@ -0,0 +1,5 @@
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := libtermux-bootstrap
LOCAL_SRC_FILES := termux-bootstrap-zip.S termux-bootstrap.c
include $(BUILD_SHARED_LIBRARY)
+18
View File
@@ -0,0 +1,18 @@
.global blob
.global blob_size
.section .rodata
blob:
#if defined __i686__
.incbin "bootstrap-i686.zip"
#elif defined __x86_64__
.incbin "bootstrap-x86_64.zip"
#elif defined __aarch64__
.incbin "bootstrap-aarch64.zip"
#elif defined __arm__
.incbin "bootstrap-arm.zip"
#else
# error Unsupported arch
#endif
1:
blob_size:
.int 1b - blob
+11
View File
@@ -0,0 +1,11 @@
#include <jni.h>
extern jbyte blob[];
extern int blob_size;
JNIEXPORT jbyteArray JNICALL Java_com_termux_app_TermuxInstaller_getZip(JNIEnv *env, __attribute__((__unused__)) jobject This)
{
jbyteArray ret = (*env)->NewByteArray(env, blob_size);
(*env)->SetByteArrayRegion(env, ret, 0, blob_size, blob);
return ret;
}
@@ -1,5 +1,9 @@
package com.termux.app;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import java.io.BufferedReader;
@@ -24,7 +28,11 @@ public final class BackgroundJob {
final Process mProcess;
public BackgroundJob(String cwd, String fileToExecute, final String[] args, final TermuxService service) {
public BackgroundJob(String cwd, String fileToExecute, final String[] args, final TermuxService service){
this(cwd, fileToExecute, args, service, null);
}
public BackgroundJob(String cwd, String fileToExecute, final String[] args, final TermuxService service, PendingIntent pendingIntent) {
String[] env = buildEnvironment(false, cwd);
if (cwd == null) cwd = TermuxService.HOME_PATH;
@@ -43,6 +51,28 @@ public final class BackgroundJob {
mProcess = process;
final int pid = getPid(mProcess);
final Bundle result = new Bundle();
final StringBuilder outResult = new StringBuilder();
final StringBuilder errResult = new StringBuilder();
Thread errThread = new Thread() {
@Override
public void run() {
InputStream stderr = mProcess.getErrorStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(stderr, StandardCharsets.UTF_8));
String line;
try {
// FIXME: Long lines.
while ((line = reader.readLine()) != null) {
errResult.append(line).append('\n');
Log.i(LOG_TAG, "[" + pid + "] stderr: " + line);
}
} catch (IOException e) {
// Ignore.
}
}
};
errThread.start();
new Thread() {
@Override
@@ -50,11 +80,13 @@ public final class BackgroundJob {
Log.i(LOG_TAG, "[" + pid + "] starting: " + processDescription);
InputStream stdout = mProcess.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(stdout, StandardCharsets.UTF_8));
String line;
try {
// FIXME: Long lines.
while ((line = reader.readLine()) != null) {
Log.i(LOG_TAG, "[" + pid + "] stdout: " + line);
outResult.append(line).append('\n');
}
} catch (IOException e) {
Log.e(LOG_TAG, "Error reading output", e);
@@ -68,29 +100,28 @@ public final class BackgroundJob {
} else {
Log.w(LOG_TAG, "[" + pid + "] exited with code: " + exitCode);
}
result.putString("stdout", outResult.toString());
result.putInt("exitCode", exitCode);
errThread.join();
result.putString("stderr", errResult.toString());
Intent data = new Intent();
data.putExtra("result", result);
if(pendingIntent != null) {
try {
pendingIntent.send(service.getApplicationContext(), Activity.RESULT_OK, data);
} catch (PendingIntent.CanceledException e) {
// The caller doesn't want the result? That's fine, just ignore
}
}
} catch (InterruptedException e) {
// Ignore.
// Ignore
}
}
}.start();
new Thread() {
@Override
public void run() {
InputStream stderr = mProcess.getErrorStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(stderr, StandardCharsets.UTF_8));
String line;
try {
// FIXME: Long lines.
while ((line = reader.readLine()) != null) {
Log.i(LOG_TAG, "[" + pid + "] stderr: " + line);
}
} catch (IOException e) {
// Ignore.
}
}
};
}
private static void addToEnvIfPresent(List<String> environment, String name) {
@@ -110,7 +141,7 @@ public final class BackgroundJob {
environment.add("TERM=xterm-256color");
environment.add("HOME=" + TermuxService.HOME_PATH);
environment.add("PREFIX=" + TermuxService.PREFIX_PATH);
environment.add("BOOTCLASSPATH" + System.getenv("BOOTCLASSPATH"));
environment.add("BOOTCLASSPATH=" + System.getenv("BOOTCLASSPATH"));
environment.add("ANDROID_ROOT=" + System.getenv("ANDROID_ROOT"));
environment.add("ANDROID_DATA=" + System.getenv("ANDROID_DATA"));
// EXTERNAL_STORAGE is needed for /system/bin/am to work on at least
@@ -139,14 +170,14 @@ public final class BackgroundJob {
try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(TermuxService.PREFIX_PATH + "/etc/apt/sources.list")))) {
String line;
while ((line = in.readLine()) != null) {
if (!line.startsWith("#") && line.contains("https://dl.bintray.com/termux/termux-packages-24")) {
return false;
if (!line.startsWith("#") && line.contains("//termux.net stable")) {
return true;
}
}
} catch (IOException e) {
Log.e(LOG_TAG, "Error trying to read sources.list", e);
}
return true;
return false;
}
public static int getPid(Process p) {
@@ -354,9 +354,13 @@ public final class ExtraKeysView extends GridLayout {
if (Settings.System.getInt(getContext().getContentResolver(),
Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) != 0) {
// Depending on DnD settings, value can be >1 but 0 means "disabled".
if (Settings.Global.getInt(getContext().getContentResolver(), "zen_mode", 0) < 1) {
if (Build.VERSION.SDK_INT >= 28) {
finalButton.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
} else {
// Perform haptic feedback only if no total silence mode enabled.
if (Settings.Global.getInt(getContext().getContentResolver(), "zen_mode", 0) != 2) {
finalButton.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
}
}
}
@@ -401,8 +405,14 @@ public final class ExtraKeysView extends GridLayout {
}
return true;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
v.setBackgroundColor(BUTTON_COLOR);
if (scheduledExecutor != null) {
scheduledExecutor.shutdownNow();
scheduledExecutor = null;
}
return true;
case MotionEvent.ACTION_UP:
v.setBackgroundColor(BUTTON_COLOR);
if (scheduledExecutor != null) {
scheduledExecutor.shutdownNow();
@@ -427,11 +437,7 @@ public final class ExtraKeysView extends GridLayout {
LayoutParams param = new GridLayout.LayoutParams();
param.width = 0;
if(Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) { // special handle api 21
param.height = (int)(37.5 * getResources().getDisplayMetrics().density + 0.5); // 37.5 equal to R.id.viewpager layout_height / rows in DP
} 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);
@@ -188,18 +188,12 @@ public final class TermuxActivity extends Activity implements ServiceConnection
}
/** For processes to access shared internal storage (/sdcard) we need this permission. */
@TargetApi(Build.VERSION_CODES.M)
public boolean ensureStoragePermissionGranted() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
return true;
} else {
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUESTCODE_PERMISSION_STORAGE);
return false;
}
} else {
// Always granted before Android 6.0.
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
return true;
} else {
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUESTCODE_PERMISSION_STORAGE);
return false;
}
}
@@ -609,7 +603,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
new AlertDialog.Builder(this).setTitle(R.string.max_terminals_reached_title).setMessage(R.string.max_terminals_reached_message)
.setPositiveButton(android.R.string.ok, null).show();
} else {
TerminalSession newSession = mTermService.createTermSession(null, null, null, failSafe);
TerminalSession currentSession = getCurrentTermSession();
String workingDirectory = (currentSession == null) ? null : currentSession.getCwd();
TerminalSession newSession = mTermService.createTermSession(null, null, workingDirectory, failSafe);
if (sessionName != null) {
newSession.mSessionName = sessionName;
}
@@ -737,6 +733,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
// 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(")");
@@ -4,7 +4,6 @@ import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.Build;
import android.os.Environment;
import android.os.UserManager;
import android.system.Os;
@@ -16,14 +15,12 @@ import com.termux.R;
import com.termux.terminal.EmulatorDebug;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@@ -38,7 +35,7 @@ import java.util.zip.ZipInputStream;
* <p/>
* (3) A staging folder, $STAGING_PREFIX, is {@link #deleteFolder(File)} if left over from broken installation below.
* <p/>
* (4) The architecture is determined and an appropriate bootstrap zip url is determined in {@link #determineZipUrl()}.
* (4) The zip file is loaded from a shared library.
* <p/>
* (5) The zip, containing entries relative to the $PREFIX, is is downloaded and extracted by a zip input stream
* continuously encountering zip file entries:
@@ -82,8 +79,8 @@ final class TermuxInstaller {
final byte[] buffer = new byte[8096];
final List<Pair<String, String>> symlinks = new ArrayList<>(50);
final URL zipUrl = determineZipUrl();
try (ZipInputStream zipInput = new ZipInputStream(zipUrl.openStream())) {
final byte[] zipBytes = loadZipBytes();
try (ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
ZipEntry zipEntry;
while ((zipEntry = zipInput.getNextEntry()) != null) {
if (zipEntry.getName().equals("SYMLINKS.txt")) {
@@ -167,34 +164,13 @@ final class TermuxInstaller {
}
}
/** Get bootstrap zip url for this systems cpu architecture. */
private static URL determineZipUrl() throws MalformedURLException {
String archName = determineTermuxArchName();
String url = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
? "https://termux.org/bootstrap-" + archName + ".zip"
: "https://termux.net/bootstrap/bootstrap-" + archName + ".zip";
return new URL(url);
public static byte[] loadZipBytes() {
// Only load the shared library when necessary to save memory usage.
System.loadLibrary("termux-bootstrap");
return getZip();
}
private static String determineTermuxArchName() {
// Note that we cannot use System.getProperty("os.arch") since that may give e.g. "aarch64"
// while a 64-bit runtime may not be installed (like on the Samsung Galaxy S5 Neo).
// Instead we search through the supported abi:s on the device, see:
// http://developer.android.com/ndk/guides/abis.html
// Note that we search for abi:s in preferred order (the ordering of the
// Build.SUPPORTED_ABIS list) to avoid e.g. installing arm on an x86 system where arm
// emulation is available.
for (String androidArch : Build.SUPPORTED_ABIS) {
switch (androidArch) {
case "arm64-v8a": return "aarch64";
case "armeabi-v7a": return "arm";
case "x86_64": return "x86_64";
case "x86": return "i686";
}
}
throw new RuntimeException("Unable to determine arch from Build.SUPPORTED_ABIS = " +
Arrays.toString(Build.SUPPORTED_ABIS));
}
public static native byte[] getZip();
/** Delete a folder and all its content or throw. Don't follow symlinks. */
static void deleteFolder(File fileOrDirectory) throws IOException {
@@ -66,6 +66,7 @@ final class TermuxPreferences {
int mBellBehaviour = BELL_VIBRATE;
boolean mBackIsEscape;
boolean mDisableVolumeVirtualKeys;
boolean mShowExtraKeys;
String[][] mExtraKeys;
@@ -198,6 +199,7 @@ final class TermuxPreferences {
}
mBackIsEscape = "escape".equals(props.getProperty("back-key", "back"));
mDisableVolumeVirtualKeys = "volume".equals(props.getProperty("volume-keys", "virtual"));
shortcuts.clear();
parseAction("shortcut.create-session", SHORTCUT_ACTION_CREATE_SESSION, props);
@@ -114,19 +114,17 @@ public final class TermuxService extends Service implements SessionChangedCallba
mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, EmulatorDebug.LOG_TAG);
mWifiLock.acquire();
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
String packageName = getPackageName();
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
Intent whitelist = new Intent();
whitelist.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
whitelist.setData(Uri.parse("package:" + packageName));
whitelist.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
String packageName = getPackageName();
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
Intent whitelist = new Intent();
whitelist.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
whitelist.setData(Uri.parse("package:" + packageName));
whitelist.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
startActivity(whitelist);
} catch (ActivityNotFoundException e) {
Log.e(EmulatorDebug.LOG_TAG, "Failed to call ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS", e);
}
try {
startActivity(whitelist);
} catch (ActivityNotFoundException e) {
Log.e(EmulatorDebug.LOG_TAG, "Failed to call ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS", e);
}
}
@@ -150,7 +148,7 @@ public final class TermuxService extends Service implements SessionChangedCallba
String cwd = intent.getStringExtra(EXTRA_CURRENT_WORKING_DIRECTORY);
if (intent.getBooleanExtra(EXTRA_EXECUTE_IN_BACKGROUND, false)) {
BackgroundJob task = new BackgroundJob(cwd, executablePath, arguments, this);
BackgroundJob task = new BackgroundJob(cwd, executablePath, arguments, this, intent.getParcelableExtra("pendingIntent"));
mBackgroundTasks.add(task);
updateNotification();
} else {
@@ -264,7 +264,9 @@ public final class TermuxViewClient implements TerminalViewClient {
/** Handle dedicated volume buttons as virtual keys if applicable. */
private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) {
InputDevice inputDevice = event.getDevice();
if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
if (mActivity.mSettings.mDisableVolumeVirtualKeys) {
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) {
@@ -71,7 +71,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
row.add(Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR));
row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(BASE_DIR));
row.add(Root.COLUMN_SUMMARY, null);
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH);
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD);
row.add(Root.COLUMN_TITLE, applicationName);
row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES);
row.add(Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.getFreeSpace());
@@ -117,6 +117,29 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
return true;
}
@Override
public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException {
File newFile = new File(parentDocumentId, displayName);
int noConflictId = 2;
while (newFile.exists()) {
newFile = new File(parentDocumentId, displayName + " (" + noConflictId++ + ")");
}
try {
boolean succeeded;
if (Document.MIME_TYPE_DIR.equals(mimeType)) {
succeeded = newFile.mkdir();
} else {
succeeded = newFile.createNewFile();
}
if (!succeeded) {
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
}
} catch (IOException e) {
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
}
return newFile.getPath();
}
@Override
public void deleteDocument(String documentId) throws FileNotFoundException {
File file = getFileForDocId(documentId);
@@ -169,6 +192,11 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
return result;
}
@Override
public boolean isChildDocument(String parentDocumentId, String documentId) {
return documentId.startsWith(parentDocumentId);
}
/**
* Get the document id given a file. This document id must be consistent across time as other
* applications may save the ID and use it to reference documents later.
@@ -220,10 +248,11 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
int flags = 0;
if (file.isDirectory()) {
if (file.isDirectory() && file.canWrite()) flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
if (file.canWrite()) flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
} else if (file.canWrite()) {
flags |= Document.FLAG_SUPPORTS_WRITE | Document.FLAG_SUPPORTS_DELETE;
flags |= Document.FLAG_SUPPORTS_WRITE;
}
if (file.getParentFile().canWrite()) flags |= Document.FLAG_SUPPORTS_DELETE;
final String displayName = file.getName();
final String mimeType = getMimeType(file);
@@ -21,6 +21,7 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.regex.Pattern;
public class TermuxFileReceiverActivity extends Activity {
@@ -36,6 +37,11 @@ public class TermuxFileReceiverActivity extends Activity {
*/
boolean mFinishOnDismissNameDialog = true;
static boolean isSharedTextAnUrl(String sharedText) {
return Patterns.WEB_URL.matcher(sharedText).matches()
|| Pattern.matches("magnet:\\?xt=urn:btih:.*?", sharedText);
}
@Override
protected void onResume() {
super.onResume();
@@ -50,7 +56,7 @@ public class TermuxFileReceiverActivity extends Activity {
final Uri sharedUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
if (sharedText != null) {
if (Patterns.WEB_URL.matcher(sharedText).matches()) {
if (isSharedTextAnUrl(sharedText)) {
handleUrlAndFinish(sharedText);
} else {
String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
-5
View File
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<!-- See https://developer.android.com/training/backup/autosyncapi.html -->
<include domain="file" path="home/backup" />
</full-backup-content>
@@ -1,25 +1,30 @@
package com.termux.app;
import junit.framework.TestCase;
import org.junit.Assert;
import org.junit.Test;
import java.util.Collections;
import java.util.LinkedHashSet;
public class TermuxActivityTest extends TestCase {
public class TermuxActivityTest {
private void assertUrlsAre(String text, String... urls) {
LinkedHashSet<String> expected = new LinkedHashSet<>();
Collections.addAll(expected, urls);
assertEquals(expected, TermuxActivity.extractUrls(text));
}
private void assertUrlsAre(String text, String... urls) {
LinkedHashSet<String> expected = new LinkedHashSet<>();
Collections.addAll(expected, urls);
Assert.assertEquals(expected, TermuxActivity.extractUrls(text));
}
public void testExtractUrls() {
assertUrlsAre("hello http://example.com world", "http://example.com");
@Test
public void testExtractUrls() {
assertUrlsAre("hello http://example.com world", "http://example.com");
assertUrlsAre("http://example.com\nhttp://another.com", "http://example.com", "http://another.com");
assertUrlsAre("http://example.com\nhttp://another.com", "http://example.com", "http://another.com");
assertUrlsAre("hello http://example.com world and http://more.example.com with secure https://more.example.com",
"http://example.com", "http://more.example.com", "https://more.example.com");
}
assertUrlsAre("hello http://example.com world and http://more.example.com with secure https://more.example.com",
"http://example.com", "http://more.example.com", "https://more.example.com");
assertUrlsAre("hello https://example.com/#bar https://example.com/foo#bar",
"https://example.com/#bar", "https://example.com/foo#bar");
}
}
@@ -0,0 +1,32 @@
package com.termux.filepicker;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import java.util.ArrayList;
import java.util.List;
@RunWith(RobolectricTestRunner.class)
public class TermuxFileReceiverActivityTest {
@Test
public void testIsSharedTextAnUrl() {
List<String> validUrls = new ArrayList<>();
validUrls.add("http://example.com");
validUrls.add("https://example.com");
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(TermuxFileReceiverActivity.isSharedTextAnUrl(url));
}
List<String> invalidUrls = new ArrayList<>();
invalidUrls.add("a test with example.com");
for (String url : invalidUrls) {
Assert.assertFalse(TermuxFileReceiverActivity.isSharedTextAnUrl(url));
}
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ buildscript {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.1'
classpath 'com.android.tools.build:gradle:3.6.1'
}
}
+1
View File
@@ -13,3 +13,4 @@
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
org.gradle.jvmargs=-Xmx2048M
android.useAndroidX=true
Binary file not shown.
+1 -2
View File
@@ -1,6 +1,5 @@
#Sun Aug 25 01:57:11 CEST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
Vendored
+31 -20
View File
@@ -1,5 +1,21 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
@@ -28,7 +44,7 @@ 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"'
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
@@ -109,8 +125,8 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
# 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"`
@@ -138,19 +154,19 @@ if $cygwin ; then
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
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" ;;
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" ;;
esac
fi
@@ -159,14 +175,9 @@ save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
APP_ARGS=`save "$@"`
# 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"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"
Vendored
+20 -1
View File
@@ -1,3 +1,19 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@@ -13,8 +29,11 @@ if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m"
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
+2 -1
View File
@@ -18,6 +18,7 @@ ext {
android {
compileSdkVersion 28
ndkVersion '21.0.6113669'
defaultConfig {
minSdkVersion 21
@@ -60,7 +61,7 @@ tasks.withType(Test) {
}
dependencies {
testImplementation 'junit:junit:4.12'
testImplementation 'junit:junit:4.13'
}
apply from: '../scripts/bintray-publish.gradle'
@@ -1267,6 +1267,7 @@ public final class TerminalEmulator {
break;
case 'c': // RIS - Reset to Initial State (http://vt100.net/docs/vt510-rm/RIS).
reset();
mMainBuffer.clearTranscript();
blockClear(0, 0, mColumns, mRows);
setCursorPosition(0, 0);
break;
@@ -1376,10 +1377,10 @@ public final class TerminalEmulator {
}
break;
case 'A': // "CSI${n}A" - Cursor up (CUU) ${n} rows.
setCursorRow(Math.max(mTopMargin, mCursorRow - getArg0(1)));
setCursorRow(Math.max(0, mCursorRow - getArg0(1)));
break;
case 'B': // "CSI${n}B" - Cursor down (CUD) ${n} rows.
setCursorRow(Math.min(mBottomMargin - 1, mCursorRow + getArg0(1)));
setCursorRow(Math.min(mRows - 1, mCursorRow + getArg0(1)));
break;
case 'C': // "CSI${n}C" - Cursor forward (CUF).
case 'a': // "CSI${n}a" - Horizontal position relative (HPR). From ISO-6428/ECMA-48.
@@ -8,6 +8,7 @@ import android.system.Os;
import android.system.OsConstants;
import android.util.Log;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
@@ -339,4 +340,26 @@ public final class TerminalSession extends TerminalOutput {
return mShellPid;
}
/** Returns the shell's working directory or null if it was unavailable. */
public String getCwd() {
if (mShellPid < 1) {
return null;
}
try {
final String cwdSymlink = String.format("/proc/%s/cwd/", mShellPid);
String outputPath = new File(cwdSymlink).getCanonicalPath();
String outputPathWithTrailingSlash = outputPath;
if (!outputPath.endsWith("/")) {
outputPathWithTrailingSlash += '/';
}
if (!cwdSymlink.equals(outputPathWithTrailingSlash)) {
return outputPath;
}
} catch (IOException | SecurityException e) {
Log.e(EmulatorDebug.LOG_TAG, "Error getting current directory", e);
}
return null;
}
}
@@ -133,8 +133,6 @@ public class CursorAndScreenTest extends TerminalTestCase {
withTerminalSized(3, 3).enterString("ABCDEFG\033[2AH").assertLinesAre("AHC", "DEF", "G ");
// If an attempt is made to move the cursor above the top margin, the cursor stops at the top margin:
withTerminalSized(3, 3).enterString("ABCDEFG\033[44AH").assertLinesAre("AHC", "DEF", "G ");
// Set top margin and validate that cursor does not go above it:
withTerminalSized(3, 3).enterString("\033[2rABCDEFG\033[44AH").assertLinesAre("ABC", "DHF", "G ");
}
public void testCursorDown() {
@@ -143,8 +141,6 @@ public class CursorAndScreenTest extends TerminalTestCase {
withTerminalSized(3, 3).enterString("AB\033[2BC").assertLinesAre("AB ", " ", " C");
// If an attempt is made to move the cursor below the bottom margin, the cursor stops at the bottom margin:
withTerminalSized(3, 3).enterString("AB\033[44BC").assertLinesAre("AB ", " ", " C");
// Set bottom margin and validate that cursor does not go above it:
withTerminalSized(3, 3).enterString("\033[1;2rAB\033[44BC").assertLinesAre("AB ", " C", " ");
}
public void testReportCursorPosition() {
@@ -107,4 +107,24 @@ public class ScrollRegionTest extends TerminalTestCase {
assertLinesAre("1 ", "2 ", "3 ", "QQ", "YY");
}
/** See https://github.com/termux/termux-app/issues/1340 */
public void testScrollRegionDoesNotLimitCursorMovement() {
withTerminalSized(6, 4)
.enterString("\033[4;7r\033[3;1Haaa\033[Axxx")
.assertLinesAre(
" ",
" xxx",
"aaa ",
" "
);
withTerminalSized(6, 4)
.enterString("\033[1;3r\033[3;1Haaa\033[Bxxx")
.assertLinesAre(
" ",
" ",
"aaa ",
" xxx"
);
}
}
+2 -2
View File
@@ -20,7 +20,7 @@ android {
compileSdkVersion 28
dependencies {
implementation "androidx.annotation:annotation:1.0.1"
implementation "androidx.annotation:annotation:1.1.0"
api project(":terminal-emulator")
}
@@ -44,7 +44,7 @@ android {
}
dependencies {
testImplementation 'junit:junit:4.12'
testImplementation 'junit:junit:4.13'
}
apply from: '../scripts/bintray-publish.gradle'
@@ -8,14 +8,14 @@ import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.SystemClock;
import android.text.Editable;
import android.text.InputType;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.accessibility.AccessibilityManager;
import android.view.ActionMode;
import android.view.HapticFeedbackConstants;
import android.view.InputDevice;
@@ -25,9 +25,16 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.PopupWindow;
import android.widget.Scroller;
import com.termux.terminal.EmulatorDebug;
@@ -35,6 +42,7 @@ import com.termux.terminal.KeyHandler;
import com.termux.terminal.TerminalBuffer;
import com.termux.terminal.TerminalEmulator;
import com.termux.terminal.TerminalSession;
import com.termux.terminal.WcWidth;
/** View displaying and interacting with a {@link TerminalSession}. */
public final class TerminalView extends View {
@@ -54,11 +62,14 @@ public final class TerminalView extends View {
/** The top row of text to display. Ranges from -activeTranscriptRows to 0. */
int mTopRow;
boolean mIsSelectingText = false, mIsDraggingLeftSelection, mInitialTextSelection;
boolean mIsSelectingText = false;
int mSelX1 = -1, mSelX2 = -1, mSelY1 = -1, mSelY2 = -1;
float mSelectionDownX, mSelectionDownY;
private ActionMode mActionMode;
private BitmapDrawable mLeftSelectionHandle, mRightSelectionHandle;
Drawable mSelectHandleLeft;
Drawable mSelectHandleRight;
final int[] mTempCoords = new int[2];
Rect mTempRect;
private SelectionModifierCursorController mSelectionModifierCursorController;
float mScaleFactor = 1.f;
final GestureAndScaleRecognizer mGestureRecognizer;
@@ -102,7 +113,7 @@ public final class TerminalView extends View {
public boolean onSingleTapUp(MotionEvent e) {
if (mEmulator == null) return true;
if (mIsSelectingText) {
toggleSelectingText(null);
stopTextSelectionMode();
return true;
}
requestFocus();
@@ -117,7 +128,7 @@ public final class TerminalView extends View {
@Override
public boolean onScroll(MotionEvent e, float distanceX, float distanceY) {
if (mEmulator == null || mIsSelectingText) return true;
if (mEmulator == null) return true;
if (mEmulator.isMouseTrackingActive() && e.isFromSource(InputDevice.SOURCE_MOUSE)) {
// If moving with mouse pointer while pressing button, report that instead of scroll.
// This means that we never report moving with button press-events for touch input,
@@ -144,7 +155,7 @@ public final class TerminalView extends View {
@Override
public boolean onFling(final MotionEvent e2, float velocityX, float velocityY) {
if (mEmulator == null || mIsSelectingText) return true;
if (mEmulator == null) return true;
// Do not start scrolling until last fling has been taken care of:
if (!mScroller.isFinished()) return true;
@@ -195,7 +206,7 @@ public final class TerminalView extends View {
if (mClient.onLongPress(e)) return;
if (!mIsSelectingText) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
toggleSelectingText(e);
startSelectingText(e);
}
}
});
@@ -287,6 +298,7 @@ public final class TerminalView extends View {
}
void sendTextToTerminal(CharSequence text) {
stopTextSelectionMode();
final int textLengthInChars = text.length();
for (int i = 0; i < textLengthInChars; i++) {
char firstChar = text.charAt(i);
@@ -368,7 +380,7 @@ public final class TerminalView extends View {
if (-mTopRow + rowShift > rowsInHistory) {
// .. unless we're hitting the end of history transcript, in which
// case we abort text selection and scroll to end.
toggleSelectingText(null);
stopTextSelectionMode();
} else {
skipScrolling = true;
mTopRow -= rowShift;
@@ -475,56 +487,7 @@ public final class TerminalView extends View {
final int action = ev.getAction();
if (mIsSelectingText) {
int cy = (int) (ev.getY() / mRenderer.mFontLineSpacing) + mTopRow;
int cx = (int) (ev.getX() / mRenderer.mFontWidth);
switch (action) {
case MotionEvent.ACTION_UP:
mInitialTextSelection = false;
break;
case MotionEvent.ACTION_DOWN:
int distanceFromSel1 = Math.abs(cx - mSelX1) + Math.abs(cy - mSelY1);
int distanceFromSel2 = Math.abs(cx - mSelX2) + Math.abs(cy - mSelY2);
mIsDraggingLeftSelection = distanceFromSel1 <= distanceFromSel2;
mSelectionDownX = ev.getX();
mSelectionDownY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
if (mInitialTextSelection) break;
float deltaX = ev.getX() - mSelectionDownX;
float deltaY = ev.getY() - mSelectionDownY;
int deltaCols = (int) Math.ceil(deltaX / mRenderer.mFontWidth);
int deltaRows = (int) Math.ceil(deltaY / mRenderer.mFontLineSpacing);
mSelectionDownX += deltaCols * mRenderer.mFontWidth;
mSelectionDownY += deltaRows * mRenderer.mFontLineSpacing;
if (mIsDraggingLeftSelection) {
mSelX1 += deltaCols;
mSelY1 += deltaRows;
} else {
mSelX2 += deltaCols;
mSelY2 += deltaRows;
}
mSelX1 = Math.min(mEmulator.mColumns, Math.max(0, mSelX1));
mSelX2 = Math.min(mEmulator.mColumns, Math.max(0, mSelX2));
if (mSelY1 == mSelY2 && mSelX1 > mSelX2 || mSelY1 > mSelY2) {
// Switch handles.
mIsDraggingLeftSelection = !mIsDraggingLeftSelection;
int tmpX1 = mSelX1, tmpY1 = mSelY1;
mSelX1 = mSelX2;
mSelY1 = mSelY2;
mSelX2 = tmpX1;
mSelY2 = tmpY1;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
mActionMode.invalidateContentRect();
invalidate();
break;
default:
break;
}
updateFloatingToolbarVisibility(ev);
mGestureRecognizer.onTouchEvent(ev);
return true;
} else if (ev.isFromSource(InputDevice.SOURCE_MOUSE)) {
@@ -562,7 +525,7 @@ public final class TerminalView extends View {
Log.i(EmulatorDebug.LOG_TAG, "onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")");
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (mIsSelectingText) {
toggleSelectingText(null);
stopTextSelectionMode();
return true;
} else if (mClient.shouldBackButtonBeMappedToEscape()) {
// Intercept back button to treat it as escape:
@@ -582,6 +545,7 @@ public final class TerminalView extends View {
if (LOG_KEY_EVENTS)
Log.i(EmulatorDebug.LOG_TAG, "onKeyDown(keyCode=" + keyCode + ", isSystem()=" + event.isSystem() + ", event=" + event + ")");
if (mEmulator == null) return true;
stopTextSelectionMode();
if (mClient.onKeyDown(keyCode, event, mTermSession)) {
invalidate();
@@ -771,59 +735,436 @@ public final class TerminalView extends View {
} else {
mRenderer.render(mEmulator, canvas, mTopRow, mSelY1, mSelY2, mSelX1, mSelX2);
if (mIsSelectingText) {
final int gripHandleWidth = mLeftSelectionHandle.getIntrinsicWidth();
final int gripHandleMargin = gripHandleWidth / 4; // See the png.
int right = Math.round((mSelX1) * mRenderer.mFontWidth) + gripHandleMargin;
int top = (mSelY1 + 1 - mTopRow) * mRenderer.mFontLineSpacing + mRenderer.mFontLineSpacingAndAscent;
mLeftSelectionHandle.setBounds(right - gripHandleWidth, top, right, top + mLeftSelectionHandle.getIntrinsicHeight());
mLeftSelectionHandle.draw(canvas);
int left = Math.round((mSelX2 + 1) * mRenderer.mFontWidth) - gripHandleMargin;
top = (mSelY2 + 1 - mTopRow) * mRenderer.mFontLineSpacing + mRenderer.mFontLineSpacingAndAscent;
mRightSelectionHandle.setBounds(left, top, left + gripHandleWidth, top + mRightSelectionHandle.getIntrinsicHeight());
mRightSelectionHandle.draw(canvas);
SelectionModifierCursorController selectionController = getSelectionController();
if (selectionController != null && selectionController.isActive()) {
selectionController.updatePosition();
}
}
}
/** Toggle text selection mode in the view. */
@TargetApi(23)
public void toggleSelectingText(MotionEvent ev) {
mIsSelectingText = !mIsSelectingText;
mClient.copyModeChanged(mIsSelectingText);
public void startSelectingText(MotionEvent ev) {
int cx = (int) (ev.getX() / mRenderer.mFontWidth);
final boolean eventFromMouse = ev.isFromSource(InputDevice.SOURCE_MOUSE);
// Offset for finger:
final int SELECT_TEXT_OFFSET_Y = eventFromMouse ? 0 : -40;
int cy = (int) ((ev.getY() + SELECT_TEXT_OFFSET_Y) / mRenderer.mFontLineSpacing) + mTopRow;
mSelX1 = mSelX2 = cx;
mSelY1 = mSelY2 = cy;
TerminalBuffer screen = mEmulator.getScreen();
if (!" ".equals(screen.getSelectedText(mSelX1, mSelY1, mSelX1, mSelY1))) {
// Selecting something other than whitespace. Expand to word.
while (mSelX1 > 0 && !"".equals(screen.getSelectedText(mSelX1 - 1, mSelY1, mSelX1 - 1, mSelY1))) {
mSelX1--;
}
while (mSelX2 < mEmulator.mColumns - 1 && !"".equals(screen.getSelectedText(mSelX2 + 1, mSelY1, mSelX2 + 1, mSelY1))) {
mSelX2++;
}
}
startTextSelectionMode();
}
public TerminalSession getCurrentSession() {
return mTermSession;
}
private CharSequence getText() {
return mEmulator.getScreen().getSelectedText(0, mTopRow, mEmulator.mColumns, mTopRow + mEmulator.mRows);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (mSelectionModifierCursorController != null) {
getViewTreeObserver().addOnTouchModeChangeListener(mSelectionModifierCursorController);
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mSelectionModifierCursorController != null) {
getViewTreeObserver().removeOnTouchModeChangeListener(mSelectionModifierCursorController);
mSelectionModifierCursorController.onDetached();
}
}
private int getCursorX(float x) {
return (int) (x / mRenderer.mFontWidth);
}
private int getCursorY(float y) {
return (int) (((y - 40) / mRenderer.mFontLineSpacing) + mTopRow);
}
private int getPointX(int cx) {
if (cx > mEmulator.mColumns) {
cx = mEmulator.mColumns;
}
return Math.round(cx * mRenderer.mFontWidth);
}
private int getPointY(int cy) {
return Math.round((cy - mTopRow) * mRenderer.mFontLineSpacing);
}
/**
* A CursorController instance can be used to control a cursor in the text.
* It is not used outside of {@link TerminalView}.
*/
private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
/**
* Makes the cursor controller visible on screen. Will be drawn by {@link #draw(Canvas)}.
* See also {@link #hide()}.
*/
void show();
/**
* Hide the cursor controller from screen.
* See also {@link #show()}.
*/
void hide();
/**
* @return true if the CursorController is currently visible
*/
boolean isActive();
/**
* Update the controller's position.
*/
void updatePosition(HandleView handle, int x, int y);
void updatePosition();
/**
* This method is called by {@link #onTouchEvent(MotionEvent)} and gives the controller
* a chance to become active and/or visible.
*
* @param event The touch event
*/
boolean onTouchEvent(MotionEvent event);
/**
* Called when the view is detached from window. Perform house keeping task, such as
* stopping Runnable thread that would otherwise keep a reference on the context, thus
* preventing the activity to be recycled.
*/
void onDetached();
}
private class HandleView extends View {
private Drawable mDrawable;
private PopupWindow mContainer;
private int mPointX;
private int mPointY;
private CursorController mController;
private boolean mIsDragging;
private float mTouchToWindowOffsetX;
private float mTouchToWindowOffsetY;
private float mHotspotX;
private float mHotspotY;
private float mTouchOffsetY;
private int mLastParentX;
private int mLastParentY;
int mHandleWidth;
private final int mOrigOrient;
private int mOrientation;
public static final int LEFT = 0;
public static final int RIGHT = 2;
private int mHandleHeight;
private long mLastTime;
public HandleView(CursorController controller, int orientation) {
super(TerminalView.this.getContext());
mController = controller;
mContainer = new PopupWindow(TerminalView.this.getContext(), null,
android.R.attr.textSelectHandleWindowStyle);
mContainer.setSplitTouchEnabled(true);
mContainer.setClippingEnabled(false);
mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
this.mOrigOrient = orientation;
setOrientation(orientation);
}
public void setOrientation(int orientation) {
mOrientation = orientation;
int handleWidth = 0;
switch (orientation) {
case LEFT: {
if (mSelectHandleLeft == null) {
mSelectHandleLeft = getContext().getDrawable(
R.drawable.text_select_handle_left_material);
}
//
mDrawable = mSelectHandleLeft;
handleWidth = mDrawable.getIntrinsicWidth();
mHotspotX = (handleWidth * 3) / 4;
break;
}
case RIGHT: {
if (mSelectHandleRight == null) {
mSelectHandleRight = getContext().getDrawable(
R.drawable.text_select_handle_right_material);
}
mDrawable = mSelectHandleRight;
handleWidth = mDrawable.getIntrinsicWidth();
mHotspotX = handleWidth / 4;
break;
}
if (mIsSelectingText) {
if (mLeftSelectionHandle == null) {
mLeftSelectionHandle = (BitmapDrawable) getContext().getDrawable(R.drawable.text_select_handle_left_material);
mRightSelectionHandle = (BitmapDrawable) getContext().getDrawable(R.drawable.text_select_handle_right_material);
}
int cx = (int) (ev.getX() / mRenderer.mFontWidth);
final boolean eventFromMouse = ev.isFromSource(InputDevice.SOURCE_MOUSE);
// Offset for finger:
final int SELECT_TEXT_OFFSET_Y = eventFromMouse ? 0 : -40;
int cy = (int) ((ev.getY() + SELECT_TEXT_OFFSET_Y) / mRenderer.mFontLineSpacing) + mTopRow;
mHandleHeight = mDrawable.getIntrinsicHeight();
mSelX1 = mSelX2 = cx;
mSelY1 = mSelY2 = cy;
mHandleWidth = handleWidth;
mTouchOffsetY = -mHandleHeight * 0.3f;
mHotspotY = 0;
invalidate();
}
TerminalBuffer screen = mEmulator.getScreen();
if (!" ".equals(screen.getSelectedText(mSelX1, mSelY1, mSelX1, mSelY1))) {
// Selecting something other than whitespace. Expand to word.
while (mSelX1 > 0 && !"".equals(screen.getSelectedText(mSelX1 - 1, mSelY1, mSelX1 - 1, mSelY1))) {
mSelX1--;
}
while (mSelX2 < mEmulator.mColumns - 1 && !"".equals(screen.getSelectedText(mSelX2 + 1, mSelY1, mSelX2 + 1, mSelY1))) {
mSelX2++;
}
public void changeOrientation(int orientation) {
if (mOrientation != orientation) {
setOrientation(orientation);
}
}
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(mDrawable.getIntrinsicWidth(),
mDrawable.getIntrinsicHeight());
}
public void show() {
if (!isPositionVisible()) {
hide();
return;
}
mContainer.setContentView(this);
final int[] coords = mTempCoords;
TerminalView.this.getLocationInWindow(coords);
coords[0] += mPointX;
coords[1] += mPointY;
mContainer.showAtLocation(TerminalView.this, 0, coords[0], coords[1]);
}
public void hide() {
mIsDragging = false;
mContainer.dismiss();
}
public boolean isShowing() {
return mContainer.isShowing();
}
private void checkChangedOrientation() {
if (!mIsDragging) {
return;
}
long millis = SystemClock.currentThreadTimeMillis();
if (millis - mLastTime < 50) {
return;
}
mLastTime = millis;
final TerminalView hostView = TerminalView.this;
final int left = hostView.getLeft();
final int right = hostView.getWidth();
final int top = hostView.getTop();
final int bottom = hostView.getHeight();
if (mTempRect == null) {
mTempRect = new Rect();
}
final Rect clip = mTempRect;
clip.left = left + TerminalView.this.getPaddingLeft();
clip.top = top + TerminalView.this.getPaddingTop();
clip.right = right - TerminalView.this.getPaddingRight();
clip.bottom = bottom - TerminalView.this.getPaddingBottom();
final ViewParent parent = hostView.getParent();
if (parent == null || !parent.getChildVisibleRect(hostView, clip, null)) {
return;
}
mInitialTextSelection = true;
mIsDraggingLeftSelection = true;
mSelectionDownX = ev.getX();
mSelectionDownY = ev.getY();
final int[] coords = mTempCoords;
hostView.getLocationInWindow(coords);
final int posX = coords[0] + mPointX;
if (posX < clip.left) {
changeOrientation(RIGHT);
} else if (posX + mHandleWidth > clip.right) {
changeOrientation(LEFT);
} else {
changeOrientation(mOrigOrient);
}
}
private boolean isPositionVisible() {
// Always show a dragging handle.
if (mIsDragging) {
return true;
}
final TerminalView hostView = TerminalView.this;
final int left = 0;
final int right = hostView.getWidth();
final int top = 0;
final int bottom = hostView.getHeight();
if (mTempRect == null) {
mTempRect = new Rect();
}
final Rect clip = mTempRect;
clip.left = left + TerminalView.this.getPaddingLeft();
clip.top = top + TerminalView.this.getPaddingTop();
clip.right = right - TerminalView.this.getPaddingRight();
clip.bottom = bottom - TerminalView.this.getPaddingBottom();
final ViewParent parent = hostView.getParent();
if (parent == null || !parent.getChildVisibleRect(hostView, clip, null)) {
return false;
}
final int[] coords = mTempCoords;
hostView.getLocationInWindow(coords);
final int posX = coords[0] + mPointX + (int) mHotspotX;
final int posY = coords[1] + mPointY + (int) mHotspotY;
return posX >= clip.left && posX <= clip.right &&
posY >= clip.top && posY <= clip.bottom;
}
private void moveTo(int x, int y) {
mPointX = x;
mPointY = y;
checkChangedOrientation();
if (isPositionVisible()) {
int[] coords = null;
if (mContainer.isShowing()) {
coords = mTempCoords;
TerminalView.this.getLocationInWindow(coords);
int x1 = coords[0] + mPointX;
int y1 = coords[1] + mPointY;
mContainer.update(x1, y1,
getWidth(), getHeight());
} else {
show();
}
if (mIsDragging) {
if (coords == null) {
coords = mTempCoords;
TerminalView.this.getLocationInWindow(coords);
}
if (coords[0] != mLastParentX || coords[1] != mLastParentY) {
mTouchToWindowOffsetX += coords[0] - mLastParentX;
mTouchToWindowOffsetY += coords[1] - mLastParentY;
mLastParentX = coords[0];
mLastParentY = coords[1];
}
}
} else {
if (isShowing()) {
hide();
}
}
}
@Override
public void onDraw(Canvas c) {
final int drawWidth = mDrawable.getIntrinsicWidth();
int height = mDrawable.getIntrinsicHeight();
mDrawable.setBounds(0, 0, drawWidth, height);
mDrawable.draw(c);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent ev) {
updateFloatingToolbarVisibility(ev);
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
final float rawX = ev.getRawX();
final float rawY = ev.getRawY();
mTouchToWindowOffsetX = rawX - mPointX;
mTouchToWindowOffsetY = rawY - mPointY;
final int[] coords = mTempCoords;
TerminalView.this.getLocationInWindow(coords);
mLastParentX = coords[0];
mLastParentY = coords[1];
mIsDragging = true;
break;
}
case MotionEvent.ACTION_MOVE: {
final float rawX = ev.getRawX();
final float rawY = ev.getRawY();
final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX;
final float newPosY = rawY - mTouchToWindowOffsetY + mHotspotY + mTouchOffsetY;
mController.updatePosition(this, Math.round(newPosX), Math.round(newPosY));
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mIsDragging = false;
}
return true;
}
public boolean isDragging() {
return mIsDragging;
}
void positionAtCursor(final int cx, final int cy) {
int left = (int) (getPointX(cx) - mHotspotX);
int bottom = getPointY(cy + 1);
moveTo(left, bottom);
}
}
private class SelectionModifierCursorController implements CursorController {
private final int mHandleHeight;
// The cursor controller images
private HandleView mStartHandle, mEndHandle;
// Whether selection anchors are active
private boolean mIsShowing;
SelectionModifierCursorController() {
mStartHandle = new HandleView(this, HandleView.LEFT);
mEndHandle = new HandleView(this, HandleView.RIGHT);
mHandleHeight = Math.max(mStartHandle.mHandleHeight, mEndHandle.mHandleHeight);
}
public void show() {
mIsShowing = true;
updatePosition();
mStartHandle.show();
mEndHandle.show();
final ActionMode.Callback callback = new ActionMode.Callback() {
@Override
@@ -865,7 +1206,7 @@ public final class TerminalView extends View {
showContextMenu();
break;
}
toggleSelectingText(null);
stopTextSelectionMode();
return true;
}
@@ -874,57 +1215,304 @@ public final class TerminalView extends View {
}
};
mActionMode = startActionMode(new ActionMode.Callback2() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
return callback.onCreateActionMode(mode, menu);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mActionMode = startActionMode(new ActionMode.Callback2() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
return callback.onCreateActionMode(mode, menu);
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
return callback.onActionItemClicked(mode, item);
}
@Override
public void onDestroyActionMode(ActionMode mode) {
// Ignore.
}
@Override
public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
int x1 = Math.round(mSelX1 * mRenderer.mFontWidth);
int x2 = Math.round(mSelX2 * mRenderer.mFontWidth);
int y1 = Math.round((mSelY1 - mTopRow) * mRenderer.mFontLineSpacing);
int y2 = Math.round((mSelY2 + 1 - mTopRow) * mRenderer.mFontLineSpacing);
if (x1 > x2) {
int tmp = x1;
x1 = x2;
x2 = tmp;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
outRect.set(x1, y1 + mHandleHeight, x2, y2 + mHandleHeight);
}
}, ActionMode.TYPE_FLOATING);
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
return callback.onActionItemClicked(mode, item);
}
}
@Override
public void onDestroyActionMode(ActionMode mode) {
// Ignore.
}
@Override
public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
int x1 = Math.round(mSelX1 * mRenderer.mFontWidth);
int x2 = Math.round(mSelX2 * mRenderer.mFontWidth);
int y1 = Math.round((mSelY1 - mTopRow) * mRenderer.mFontLineSpacing);
int y2 = Math.round((mSelY2 + 1 - mTopRow) * mRenderer.mFontLineSpacing);
outRect.set(Math.min(x1, x2), y1, Math.max(x1, x2), y2);
}
}, ActionMode.TYPE_FLOATING);
} else {
mActionMode = startActionMode(callback);
public void hide() {
mStartHandle.hide();
mEndHandle.hide();
mIsShowing = false;
if (mActionMode != null) {
// This will hide the mSelectionModifierCursorController
mActionMode.finish();
}
}
public boolean isActive() {
return mIsShowing;
}
public void updatePosition(HandleView handle, int x, int y) {
TerminalBuffer screen = mEmulator.getScreen();
final int scrollRows = screen.getActiveRows() - mEmulator.mRows;
if (handle == mStartHandle) {
mSelX1 = getCursorX(x);
mSelY1 = getCursorY(y);
if (mSelX1 < 0) {
mSelX1 = 0;
}
if (mSelY1 < -scrollRows) {
mSelY1 = -scrollRows;
} else if (mSelY1 > mEmulator.mRows - 1) {
mSelY1 = mEmulator.mRows - 1;
}
if (mSelY1 > mSelY2) {
mSelY1 = mSelY2;
}
if (mSelY1 == mSelY2 && mSelX1 > mSelX2) {
mSelX1 = mSelX2;
}
if (!mEmulator.isAlternateBufferActive()) {
if (mSelY1 <= mTopRow) {
mTopRow--;
if (mTopRow < -scrollRows) {
mTopRow = -scrollRows;
}
} else if (mSelY1 >= mTopRow + mEmulator.mRows) {
mTopRow++;
if (mTopRow > 0) {
mTopRow = 0;
}
}
}
mSelX1 = getValidCurX(screen, mSelY1, mSelX1);
} else {
mSelX2 = getCursorX(x);
mSelY2 = getCursorY(y);
if (mSelX2 < 0) {
mSelX2 = 0;
}
if (mSelY2 < -scrollRows) {
mSelY2 = -scrollRows;
} else if (mSelY2 > mEmulator.mRows - 1) {
mSelY2 = mEmulator.mRows - 1;
}
if (mSelY1 > mSelY2) {
mSelY2 = mSelY1;
}
if (mSelY1 == mSelY2 && mSelX1 > mSelX2) {
mSelX2 = mSelX1;
}
if (!mEmulator.isAlternateBufferActive()) {
if (mSelY2 <= mTopRow) {
mTopRow--;
if (mTopRow < -scrollRows) {
mTopRow = -scrollRows;
}
} else if (mSelY2 >= mTopRow + mEmulator.mRows) {
mTopRow++;
if (mTopRow > 0) {
mTopRow = 0;
}
}
}
mSelX2 = getValidCurX(screen, mSelY2, mSelX2);
}
invalidate();
} else {
mActionMode.finish();
}
private int getValidCurX(TerminalBuffer screen, int cy, int cx) {
String line = screen.getSelectedText(0, cy, cx, cy);
if (!TextUtils.isEmpty(line)) {
int col = 0;
for (int i = 0, len = line.length(); i < len; i++) {
char ch1 = line.charAt(i);
if (ch1 == 0) {
break;
}
int wc;
if (Character.isHighSurrogate(ch1) && i + 1 < len) {
char ch2 = line.charAt(++i);
wc = WcWidth.width(Character.toCodePoint(ch1, ch2));
} else {
wc = WcWidth.width(ch1);
}
final int cend = col + wc;
if (cx > col && cx < cend) {
return cend;
}
if (cend == col) {
return col;
}
col = cend;
}
}
return cx;
}
public void updatePosition() {
if (!isActive()) {
return;
}
mStartHandle.positionAtCursor(mSelX1, mSelY1);
mEndHandle.positionAtCursor(mSelX2 + 1, mSelY2); //bug
if (mActionMode != null) {
mActionMode.invalidate();
}
}
public boolean onTouchEvent(MotionEvent event) {
return false;
}
/**
* @return true iff this controller is currently used to move the selection start.
*/
public boolean isSelectionStartDragged() {
return mStartHandle.isDragging();
}
public boolean isSelectionEndDragged() {
return mEndHandle.isDragging();
}
public void onTouchModeChanged(boolean isInTouchMode) {
if (!isInTouchMode) {
hide();
}
}
@Override
public void onDetached() {
}
}
SelectionModifierCursorController getSelectionController() {
if (mSelectionModifierCursorController == null) {
mSelectionModifierCursorController = new SelectionModifierCursorController();
final ViewTreeObserver observer = getViewTreeObserver();
if (observer != null) {
observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
}
}
return mSelectionModifierCursorController;
}
private void hideSelectionModifierCursorController() {
if (mSelectionModifierCursorController != null && mSelectionModifierCursorController.isActive()) {
mSelectionModifierCursorController.hide();
}
}
private void startTextSelectionMode() {
if (!requestFocus()) {
return;
}
getSelectionController().show();
mIsSelectingText = true;
mClient.copyModeChanged(mIsSelectingText);
invalidate();
}
private void stopTextSelectionMode() {
if (mIsSelectingText) {
hideSelectionModifierCursorController();
mSelX1 = mSelY1 = mSelX2 = mSelY2 = -1;
mIsSelectingText = false;
mClient.copyModeChanged(mIsSelectingText);
invalidate();
}
}
public TerminalSession getCurrentSession() {
return mTermSession;
private final Runnable mShowFloatingToolbar = new Runnable() {
@Override
public void run() {
if (mActionMode != null) {
mActionMode.hide(0); // hide off.
}
}
};
void hideFloatingToolbar(int duration) {
if (mActionMode != null) {
removeCallbacks(mShowFloatingToolbar);
mActionMode.hide(duration);
}
}
private CharSequence getText() {
return mEmulator.getScreen().getSelectedText(0, mTopRow, mEmulator.mColumns, mTopRow +mEmulator.mRows);
private void showFloatingToolbar() {
if (mActionMode != null) {
int delay = ViewConfiguration.getDoubleTapTimeout();
postDelayed(mShowFloatingToolbar, delay);
}
}
private void updateFloatingToolbarVisibility(MotionEvent event) {
if (mActionMode != null) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_MOVE:
hideFloatingToolbar(-1);
break;
case MotionEvent.ACTION_UP: // fall through
case MotionEvent.ACTION_CANCEL:
showFloatingToolbar();
}
}
}
}