Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93724b7aa6 | ||
|
|
5d06f040e8 | ||
|
|
ed9afa082a | ||
|
|
9703bd31ad | ||
|
|
9749f25eba | ||
|
|
453b838b24 | ||
|
|
9fdf2a49fd | ||
|
|
3270506bff | ||
|
|
a240f4cf45 | ||
|
|
4647beb0d2 | ||
|
|
d92e806461 | ||
|
|
b75cf0bb84 | ||
|
|
f928efed4e | ||
|
|
36db64d585 | ||
|
|
b8f0430699 | ||
|
|
90e6260d5e | ||
|
|
566d656c16 | ||
|
|
b729085d52 | ||
|
|
a94bcb02f1 | ||
|
|
e28be01dc2 | ||
|
|
7f7c1efac1 | ||
|
|
76df44e6bb | ||
|
|
fd13f3f98d | ||
|
|
269c3cafb0 | ||
|
|
662b5cace6 | ||
|
|
3c01b09fff | ||
|
|
1ce2db9929 | ||
|
|
c987ff718a | ||
|
|
9e295b105c | ||
|
|
37b3a9f8db | ||
|
|
490853e427 | ||
|
|
9ebedc4740 | ||
|
|
82730d9e08 | ||
|
|
3d2756f376 | ||
|
|
6db51bdec0 | ||
|
|
c238c8bddb | ||
|
|
60e1556871 | ||
|
|
e47bd31681 | ||
|
|
951df6b4e2 | ||
|
|
fcd3bc1133 | ||
|
|
f4db9ff212 | ||
|
|
87841886d4 | ||
|
|
cf883f5f05 | ||
|
|
677d75e173 | ||
|
|
fdb3764f5c | ||
|
|
937eb350b2 | ||
|
|
3b4ece6bd8 | ||
|
|
35a4fdacbe |
20
.cirrus.yml
20
.cirrus.yml
@@ -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"
|
|
||||||
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,20 +1,35 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
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**
|
**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**
|
**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**
|
**Expected behavior**
|
||||||
|
<!--
|
||||||
A clear and concise description of what you expected to happen.
|
A clear and concise description of what you expected to happen.
|
||||||
|
-->
|
||||||
|
|
||||||
**Additional information**
|
**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:
|
||||||
|
|||||||
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,12 +1,22 @@
|
|||||||
---
|
---
|
||||||
name: Feature request
|
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**
|
**Feature description**
|
||||||
|
<!--
|
||||||
Describe the feature and why you want it.
|
Describe the feature and why you want it.
|
||||||
|
-->
|
||||||
|
|
||||||
**Reference implementation**
|
**Reference implementation**
|
||||||
|
|
||||||
Does another app/terminal emulator have this feature?
|
Does another app/terminal emulator have this feature?
|
||||||
Provide links to more background information
|
Provide links to more background information.
|
||||||
|
|||||||
18
.github/workflows/debug_build.yml
vendored
Normal file
18
.github/workflows/debug_build.yml
vendored
Normal 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
.github/workflows/run_tests.yml
vendored
Normal file
13
.github/workflows/run_tests.yml
vendored
Normal 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
|
||||||
10
README.md
10
README.md
@@ -1,6 +1,7 @@
|
|||||||
# Termux application
|
# Termux application
|
||||||
|
|
||||||
[](https://cirrus-ci.com/termux/termux-app)
|
[](https://github.com/termux/termux-app/actions)
|
||||||
|
[](https://github.com/termux/termux-app/actions)
|
||||||
[](https://gitter.im/termux/termux)
|
[](https://gitter.im/termux/termux)
|
||||||
|
|
||||||
[Termux](https://termux.com) is an Android terminal application and Linux environment.
|
[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/)
|
- [F-Droid](https://f-droid.org/en/packages/com.termux/)
|
||||||
- [Kali Nethunter Store](https://store.nethunter.com/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
|
Additionally we provide per-commit debug builds for those who want to try
|
||||||
features ready to be included in future versions. Such build can be obtained
|
out the latest features or test their pull request. This 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).
|
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
|
Signature keys of all offered builds are different. Before you switch the
|
||||||
installation source, you will have to uninstall the Termux application and
|
installation source, you will have to uninstall the Termux application and
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ android {
|
|||||||
compileSdkVersion 28
|
compileSdkVersion 28
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "androidx.annotation:annotation:1.0.1"
|
implementation "androidx.annotation:annotation:1.1.0"
|
||||||
implementation "androidx.viewpager:viewpager:1.0.0"
|
implementation "androidx.viewpager:viewpager:1.0.0"
|
||||||
implementation "androidx.drawerlayout:drawerlayout:1.0.0"
|
implementation "androidx.drawerlayout:drawerlayout:1.0.0"
|
||||||
implementation project(":terminal-view")
|
implementation project(":terminal-view")
|
||||||
@@ -16,8 +16,8 @@ android {
|
|||||||
applicationId "com.termux"
|
applicationId "com.termux"
|
||||||
minSdkVersion 24
|
minSdkVersion 24
|
||||||
targetSdkVersion 28
|
targetSdkVersion 28
|
||||||
versionCode 82
|
versionCode 92
|
||||||
versionName "0.82"
|
versionName "0.92"
|
||||||
|
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
ndkBuild {
|
ndkBuild {
|
||||||
@@ -62,10 +62,17 @@ android {
|
|||||||
path "src/main/cpp/Android.mk"
|
path "src/main/cpp/Android.mk"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
includeAndroidResources = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.13'
|
||||||
|
testImplementation 'org.robolectric:robolectric:4.3'
|
||||||
}
|
}
|
||||||
|
|
||||||
task versionName {
|
task versionName {
|
||||||
@@ -125,11 +132,11 @@ clean {
|
|||||||
|
|
||||||
task downloadBootstraps(){
|
task downloadBootstraps(){
|
||||||
doLast {
|
doLast {
|
||||||
def version = 18
|
def version = 20
|
||||||
downloadBootstrap("aarch64", "1a4c08a696d452b58f69102428239ec0c07521c0ca9f48b23ef70ae0e5e3d4f8", version)
|
downloadBootstrap("aarch64", "2ea6aaff12d8316223e5c1f22719d20633fae669d6461a6802b67b4adbe796de", version)
|
||||||
downloadBootstrap("arm", "bff11f2c7e9c1055a22fc5f20bb7507b75f6034e0f5d591ec6725b3407981b85", version)
|
downloadBootstrap("arm", "8a3a7e8adeff8eb769b03cad947f81b8c42b7c4c8edeea37c71a9d7abd9de99c", version)
|
||||||
downloadBootstrap("i686", "6fb93020db2807337d82a1537e24612400cacbd10cf4bccaeb0714d51e653da1", version)
|
downloadBootstrap("i686", "b3e1f8e3ccb695d6fab7714c62b2028fbc37187ccfaff0a9f6bd64f738bc5adc", version)
|
||||||
downloadBootstrap("x86_64", "a6067e5decc486dcad190c1ed9e15366c798e5e7d9b9b9ee6b4b8231290524c3", version)
|
downloadBootstrap("x86_64", "2a9f6adbfb6f5e7c0bd03e022856a140768fa25ada850384d635c25c8e966ea3", version)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,4 +145,3 @@ afterEvaluate {
|
|||||||
variant.javaCompileProvider.get().dependsOn(downloadBootstraps)
|
variant.javaCompileProvider.get().dependsOn(downloadBootstraps)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,11 +31,11 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.termux.app.TermuxActivity"
|
android:name="com.termux.app.TermuxActivity"
|
||||||
android:label="@string/application_name"
|
android:label="@string/application_name"
|
||||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|uiMode|keyboard|keyboardHidden|navigation"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:windowSoftInputMode="adjustResize|stateAlwaysVisible" >
|
android:windowSoftInputMode="adjustResize|stateAlwaysVisible" >
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
@@ -120,8 +120,8 @@
|
|||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:grantUriPermissions="true"
|
android:grantUriPermissions="true"
|
||||||
android:name="com.termux.app.TermuxOpenReceiver$ContentProvider" />
|
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>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package com.termux.app;
|
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 android.util.Log;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
@@ -24,7 +28,11 @@ public final class BackgroundJob {
|
|||||||
|
|
||||||
final Process mProcess;
|
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);
|
String[] env = buildEnvironment(false, cwd);
|
||||||
if (cwd == null) cwd = TermuxService.HOME_PATH;
|
if (cwd == null) cwd = TermuxService.HOME_PATH;
|
||||||
|
|
||||||
@@ -43,6 +51,28 @@ public final class BackgroundJob {
|
|||||||
|
|
||||||
mProcess = process;
|
mProcess = process;
|
||||||
final int pid = getPid(mProcess);
|
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() {
|
new Thread() {
|
||||||
@Override
|
@Override
|
||||||
@@ -50,11 +80,13 @@ public final class BackgroundJob {
|
|||||||
Log.i(LOG_TAG, "[" + pid + "] starting: " + processDescription);
|
Log.i(LOG_TAG, "[" + pid + "] starting: " + processDescription);
|
||||||
InputStream stdout = mProcess.getInputStream();
|
InputStream stdout = mProcess.getInputStream();
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(stdout, StandardCharsets.UTF_8));
|
BufferedReader reader = new BufferedReader(new InputStreamReader(stdout, StandardCharsets.UTF_8));
|
||||||
|
|
||||||
String line;
|
String line;
|
||||||
try {
|
try {
|
||||||
// FIXME: Long lines.
|
// FIXME: Long lines.
|
||||||
while ((line = reader.readLine()) != null) {
|
while ((line = reader.readLine()) != null) {
|
||||||
Log.i(LOG_TAG, "[" + pid + "] stdout: " + line);
|
Log.i(LOG_TAG, "[" + pid + "] stdout: " + line);
|
||||||
|
outResult.append(line).append('\n');
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.e(LOG_TAG, "Error reading output", e);
|
Log.e(LOG_TAG, "Error reading output", e);
|
||||||
@@ -68,29 +100,28 @@ public final class BackgroundJob {
|
|||||||
} else {
|
} else {
|
||||||
Log.w(LOG_TAG, "[" + pid + "] exited with code: " + exitCode);
|
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) {
|
} catch (InterruptedException e) {
|
||||||
// Ignore.
|
// Ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.start();
|
}.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) {
|
private static void addToEnvIfPresent(List<String> environment, String name) {
|
||||||
@@ -110,7 +141,7 @@ public final class BackgroundJob {
|
|||||||
environment.add("TERM=xterm-256color");
|
environment.add("TERM=xterm-256color");
|
||||||
environment.add("HOME=" + TermuxService.HOME_PATH);
|
environment.add("HOME=" + TermuxService.HOME_PATH);
|
||||||
environment.add("PREFIX=" + TermuxService.PREFIX_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_ROOT=" + System.getenv("ANDROID_ROOT"));
|
||||||
environment.add("ANDROID_DATA=" + System.getenv("ANDROID_DATA"));
|
environment.add("ANDROID_DATA=" + System.getenv("ANDROID_DATA"));
|
||||||
// EXTERNAL_STORAGE is needed for /system/bin/am to work on at least
|
// EXTERNAL_STORAGE is needed for /system/bin/am to work on at least
|
||||||
|
|||||||
@@ -354,9 +354,13 @@ public final class ExtraKeysView extends GridLayout {
|
|||||||
if (Settings.System.getInt(getContext().getContentResolver(),
|
if (Settings.System.getInt(getContext().getContentResolver(),
|
||||||
Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) != 0) {
|
Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) != 0) {
|
||||||
|
|
||||||
// Depending on DnD settings, value can be >1 but 0 means "disabled".
|
if (Build.VERSION.SDK_INT >= 28) {
|
||||||
if (Settings.Global.getInt(getContext().getContentResolver(), "zen_mode", 0) < 1) {
|
|
||||||
finalButton.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
|
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;
|
return true;
|
||||||
|
|
||||||
case MotionEvent.ACTION_UP:
|
|
||||||
case MotionEvent.ACTION_CANCEL:
|
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);
|
v.setBackgroundColor(BUTTON_COLOR);
|
||||||
if (scheduledExecutor != null) {
|
if (scheduledExecutor != null) {
|
||||||
scheduledExecutor.shutdownNow();
|
scheduledExecutor.shutdownNow();
|
||||||
|
|||||||
@@ -603,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)
|
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();
|
.setPositiveButton(android.R.string.ok, null).show();
|
||||||
} else {
|
} 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) {
|
if (sessionName != null) {
|
||||||
newSession.mSessionName = sessionName;
|
newSession.mSessionName = sessionName;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.app.Activity;
|
|||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
import android.os.UserManager;
|
import android.os.UserManager;
|
||||||
import android.system.Os;
|
import android.system.Os;
|
||||||
@@ -21,10 +20,7 @@ import java.io.File;
|
|||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.net.MalformedURLException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipInputStream;
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ final class TermuxPreferences {
|
|||||||
int mBellBehaviour = BELL_VIBRATE;
|
int mBellBehaviour = BELL_VIBRATE;
|
||||||
|
|
||||||
boolean mBackIsEscape;
|
boolean mBackIsEscape;
|
||||||
|
boolean mDisableVolumeVirtualKeys;
|
||||||
boolean mShowExtraKeys;
|
boolean mShowExtraKeys;
|
||||||
|
|
||||||
String[][] mExtraKeys;
|
String[][] mExtraKeys;
|
||||||
@@ -198,6 +199,7 @@ final class TermuxPreferences {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mBackIsEscape = "escape".equals(props.getProperty("back-key", "back"));
|
mBackIsEscape = "escape".equals(props.getProperty("back-key", "back"));
|
||||||
|
mDisableVolumeVirtualKeys = "volume".equals(props.getProperty("volume-keys", "virtual"));
|
||||||
|
|
||||||
shortcuts.clear();
|
shortcuts.clear();
|
||||||
parseAction("shortcut.create-session", SHORTCUT_ACTION_CREATE_SESSION, props);
|
parseAction("shortcut.create-session", SHORTCUT_ACTION_CREATE_SESSION, props);
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
|||||||
String cwd = intent.getStringExtra(EXTRA_CURRENT_WORKING_DIRECTORY);
|
String cwd = intent.getStringExtra(EXTRA_CURRENT_WORKING_DIRECTORY);
|
||||||
|
|
||||||
if (intent.getBooleanExtra(EXTRA_EXECUTE_IN_BACKGROUND, false)) {
|
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);
|
mBackgroundTasks.add(task);
|
||||||
updateNotification();
|
updateNotification();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -264,7 +264,9 @@ public final class TermuxViewClient implements TerminalViewClient {
|
|||||||
/** Handle dedicated volume buttons as virtual keys if applicable. */
|
/** Handle dedicated volume buttons as virtual keys if applicable. */
|
||||||
private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) {
|
private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) {
|
||||||
InputDevice inputDevice = event.getDevice();
|
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.
|
// Do not steal dedicated buttons from a full external keyboard.
|
||||||
return false;
|
return false;
|
||||||
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
|
} 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_ROOT_ID, getDocIdForFile(BASE_DIR));
|
||||||
row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(BASE_DIR));
|
row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(BASE_DIR));
|
||||||
row.add(Root.COLUMN_SUMMARY, null);
|
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_TITLE, applicationName);
|
||||||
row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES);
|
row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES);
|
||||||
row.add(Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.getFreeSpace());
|
row.add(Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.getFreeSpace());
|
||||||
@@ -117,6 +117,29 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
|||||||
return true;
|
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
|
@Override
|
||||||
public void deleteDocument(String documentId) throws FileNotFoundException {
|
public void deleteDocument(String documentId) throws FileNotFoundException {
|
||||||
File file = getFileForDocId(documentId);
|
File file = getFileForDocId(documentId);
|
||||||
@@ -169,6 +192,11 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
|||||||
return result;
|
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
|
* 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.
|
* applications may save the ID and use it to reference documents later.
|
||||||
@@ -220,10 +248,11 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
|||||||
|
|
||||||
int flags = 0;
|
int flags = 0;
|
||||||
if (file.isDirectory()) {
|
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()) {
|
} 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 displayName = file.getName();
|
||||||
final String mimeType = getMimeType(file);
|
final String mimeType = getMimeType(file);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import java.io.FileOutputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class TermuxFileReceiverActivity extends Activity {
|
public class TermuxFileReceiverActivity extends Activity {
|
||||||
|
|
||||||
@@ -36,6 +37,11 @@ public class TermuxFileReceiverActivity extends Activity {
|
|||||||
*/
|
*/
|
||||||
boolean mFinishOnDismissNameDialog = true;
|
boolean mFinishOnDismissNameDialog = true;
|
||||||
|
|
||||||
|
static boolean isSharedTextAnUrl(String sharedText) {
|
||||||
|
return Patterns.WEB_URL.matcher(sharedText).matches()
|
||||||
|
|| Pattern.matches("magnet:\\?xt=urn:btih:.*?", sharedText);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
@@ -50,7 +56,7 @@ public class TermuxFileReceiverActivity extends Activity {
|
|||||||
final Uri sharedUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
final Uri sharedUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||||
|
|
||||||
if (sharedText != null) {
|
if (sharedText != null) {
|
||||||
if (Patterns.WEB_URL.matcher(sharedText).matches()) {
|
if (isSharedTextAnUrl(sharedText)) {
|
||||||
handleUrlAndFinish(sharedText);
|
handleUrlAndFinish(sharedText);
|
||||||
} else {
|
} else {
|
||||||
String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
|
String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
package com.termux.app;
|
package com.termux.app;
|
||||||
|
|
||||||
import junit.framework.TestCase;
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
|
|
||||||
public class TermuxActivityTest extends TestCase {
|
public class TermuxActivityTest {
|
||||||
|
|
||||||
private void assertUrlsAre(String text, String... urls) {
|
private void assertUrlsAre(String text, String... urls) {
|
||||||
LinkedHashSet<String> expected = new LinkedHashSet<>();
|
LinkedHashSet<String> expected = new LinkedHashSet<>();
|
||||||
Collections.addAll(expected, urls);
|
Collections.addAll(expected, urls);
|
||||||
assertEquals(expected, TermuxActivity.extractUrls(text));
|
Assert.assertEquals(expected, TermuxActivity.extractUrls(text));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testExtractUrls() {
|
@Test
|
||||||
assertUrlsAre("hello http://example.com world", "http://example.com");
|
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",
|
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");
|
"http://example.com", "http://more.example.com", "https://more.example.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ buildscript {
|
|||||||
google()
|
google()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.5.2'
|
classpath 'com.android.tools.build:gradle:3.5.3'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
3
gradle/wrapper/gradle-wrapper.properties
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,5 @@
|
|||||||
#Sun Aug 25 01:57:11 CEST 2019
|
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
|
|
||||||
|
|||||||
51
gradlew
vendored
51
gradlew
vendored
@@ -1,5 +1,21 @@
|
|||||||
#!/usr/bin/env sh
|
#!/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
|
## Gradle start up script for UN*X
|
||||||
@@ -28,7 +44,7 @@ APP_NAME="Gradle"
|
|||||||
APP_BASE_NAME=`basename "$0"`
|
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.
|
# 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.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD="maximum"
|
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\""
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# For Cygwin, switch paths to Windows format before running java
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
if $cygwin ; then
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
@@ -138,19 +154,19 @@ if $cygwin ; then
|
|||||||
else
|
else
|
||||||
eval `echo args$i`="\"$arg\""
|
eval `echo args$i`="\"$arg\""
|
||||||
fi
|
fi
|
||||||
i=$((i+1))
|
i=`expr $i + 1`
|
||||||
done
|
done
|
||||||
case $i in
|
case $i in
|
||||||
(0) set -- ;;
|
0) set -- ;;
|
||||||
(1) set -- "$args0" ;;
|
1) set -- "$args0" ;;
|
||||||
(2) set -- "$args0" "$args1" ;;
|
2) set -- "$args0" "$args1" ;;
|
||||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -159,14 +175,9 @@ save () {
|
|||||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
echo " "
|
echo " "
|
||||||
}
|
}
|
||||||
APP_ARGS=$(save "$@")
|
APP_ARGS=`save "$@"`
|
||||||
|
|
||||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
# 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"
|
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" "$@"
|
exec "$JAVACMD" "$@"
|
||||||
|
|||||||
18
gradlew.bat
vendored
18
gradlew.bat
vendored
@@ -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
|
@if "%DEBUG%" == "" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@rem
|
@rem
|
||||||
@@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0
|
|||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
@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
|
@rem Find java.exe
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ tasks.withType(Test) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.13'
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: '../scripts/bintray-publish.gradle'
|
apply from: '../scripts/bintray-publish.gradle'
|
||||||
|
|||||||
@@ -1267,6 +1267,7 @@ public final class TerminalEmulator {
|
|||||||
break;
|
break;
|
||||||
case 'c': // RIS - Reset to Initial State (http://vt100.net/docs/vt510-rm/RIS).
|
case 'c': // RIS - Reset to Initial State (http://vt100.net/docs/vt510-rm/RIS).
|
||||||
reset();
|
reset();
|
||||||
|
mMainBuffer.clearTranscript();
|
||||||
blockClear(0, 0, mColumns, mRows);
|
blockClear(0, 0, mColumns, mRows);
|
||||||
setCursorPosition(0, 0);
|
setCursorPosition(0, 0);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import android.system.Os;
|
|||||||
import android.system.OsConstants;
|
import android.system.OsConstants;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.io.FileDescriptor;
|
import java.io.FileDescriptor;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
@@ -339,4 +340,26 @@ public final class TerminalSession extends TerminalOutput {
|
|||||||
return mShellPid;
|
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 ");
|
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:
|
// 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 ");
|
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() {
|
public void testCursorDown() {
|
||||||
@@ -143,8 +141,6 @@ public class CursorAndScreenTest extends TerminalTestCase {
|
|||||||
withTerminalSized(3, 3).enterString("AB\033[2BC").assertLinesAre("AB ", " ", " C");
|
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:
|
// 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");
|
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() {
|
public void testReportCursorPosition() {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ android {
|
|||||||
compileSdkVersion 28
|
compileSdkVersion 28
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "androidx.annotation:annotation:1.0.1"
|
implementation "androidx.annotation:annotation:1.1.0"
|
||||||
api project(":terminal-emulator")
|
api project(":terminal-emulator")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.13'
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: '../scripts/bintray-publish.gradle'
|
apply from: '../scripts/bintray-publish.gradle'
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ import android.content.Context;
|
|||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.graphics.Typeface;
|
import android.graphics.Typeface;
|
||||||
import android.graphics.drawable.BitmapDrawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.os.SystemClock;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.accessibility.AccessibilityManager;
|
|
||||||
import android.view.ActionMode;
|
import android.view.ActionMode;
|
||||||
import android.view.HapticFeedbackConstants;
|
import android.view.HapticFeedbackConstants;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
@@ -25,9 +25,16 @@ import android.view.Menu;
|
|||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
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.BaseInputConnection;
|
||||||
import android.view.inputmethod.EditorInfo;
|
import android.view.inputmethod.EditorInfo;
|
||||||
import android.view.inputmethod.InputConnection;
|
import android.view.inputmethod.InputConnection;
|
||||||
|
import android.widget.PopupWindow;
|
||||||
import android.widget.Scroller;
|
import android.widget.Scroller;
|
||||||
|
|
||||||
import com.termux.terminal.EmulatorDebug;
|
import com.termux.terminal.EmulatorDebug;
|
||||||
@@ -35,6 +42,7 @@ import com.termux.terminal.KeyHandler;
|
|||||||
import com.termux.terminal.TerminalBuffer;
|
import com.termux.terminal.TerminalBuffer;
|
||||||
import com.termux.terminal.TerminalEmulator;
|
import com.termux.terminal.TerminalEmulator;
|
||||||
import com.termux.terminal.TerminalSession;
|
import com.termux.terminal.TerminalSession;
|
||||||
|
import com.termux.terminal.WcWidth;
|
||||||
|
|
||||||
/** View displaying and interacting with a {@link TerminalSession}. */
|
/** View displaying and interacting with a {@link TerminalSession}. */
|
||||||
public final class TerminalView extends View {
|
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. */
|
/** The top row of text to display. Ranges from -activeTranscriptRows to 0. */
|
||||||
int mTopRow;
|
int mTopRow;
|
||||||
|
|
||||||
boolean mIsSelectingText = false, mIsDraggingLeftSelection, mInitialTextSelection;
|
boolean mIsSelectingText = false;
|
||||||
int mSelX1 = -1, mSelX2 = -1, mSelY1 = -1, mSelY2 = -1;
|
int mSelX1 = -1, mSelX2 = -1, mSelY1 = -1, mSelY2 = -1;
|
||||||
float mSelectionDownX, mSelectionDownY;
|
|
||||||
private ActionMode mActionMode;
|
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;
|
float mScaleFactor = 1.f;
|
||||||
final GestureAndScaleRecognizer mGestureRecognizer;
|
final GestureAndScaleRecognizer mGestureRecognizer;
|
||||||
@@ -102,7 +113,7 @@ public final class TerminalView extends View {
|
|||||||
public boolean onSingleTapUp(MotionEvent e) {
|
public boolean onSingleTapUp(MotionEvent e) {
|
||||||
if (mEmulator == null) return true;
|
if (mEmulator == null) return true;
|
||||||
if (mIsSelectingText) {
|
if (mIsSelectingText) {
|
||||||
toggleSelectingText(null);
|
stopTextSelectionMode();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
requestFocus();
|
requestFocus();
|
||||||
@@ -117,7 +128,7 @@ public final class TerminalView extends View {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onScroll(MotionEvent e, float distanceX, float distanceY) {
|
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 (mEmulator.isMouseTrackingActive() && e.isFromSource(InputDevice.SOURCE_MOUSE)) {
|
||||||
// If moving with mouse pointer while pressing button, report that instead of scroll.
|
// 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,
|
// 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
|
@Override
|
||||||
public boolean onFling(final MotionEvent e2, float velocityX, float velocityY) {
|
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:
|
// Do not start scrolling until last fling has been taken care of:
|
||||||
if (!mScroller.isFinished()) return true;
|
if (!mScroller.isFinished()) return true;
|
||||||
|
|
||||||
@@ -195,7 +206,7 @@ public final class TerminalView extends View {
|
|||||||
if (mClient.onLongPress(e)) return;
|
if (mClient.onLongPress(e)) return;
|
||||||
if (!mIsSelectingText) {
|
if (!mIsSelectingText) {
|
||||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
||||||
toggleSelectingText(e);
|
startSelectingText(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -287,6 +298,7 @@ public final class TerminalView extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void sendTextToTerminal(CharSequence text) {
|
void sendTextToTerminal(CharSequence text) {
|
||||||
|
stopTextSelectionMode();
|
||||||
final int textLengthInChars = text.length();
|
final int textLengthInChars = text.length();
|
||||||
for (int i = 0; i < textLengthInChars; i++) {
|
for (int i = 0; i < textLengthInChars; i++) {
|
||||||
char firstChar = text.charAt(i);
|
char firstChar = text.charAt(i);
|
||||||
@@ -368,7 +380,7 @@ public final class TerminalView extends View {
|
|||||||
if (-mTopRow + rowShift > rowsInHistory) {
|
if (-mTopRow + rowShift > rowsInHistory) {
|
||||||
// .. unless we're hitting the end of history transcript, in which
|
// .. unless we're hitting the end of history transcript, in which
|
||||||
// case we abort text selection and scroll to end.
|
// case we abort text selection and scroll to end.
|
||||||
toggleSelectingText(null);
|
stopTextSelectionMode();
|
||||||
} else {
|
} else {
|
||||||
skipScrolling = true;
|
skipScrolling = true;
|
||||||
mTopRow -= rowShift;
|
mTopRow -= rowShift;
|
||||||
@@ -475,55 +487,7 @@ public final class TerminalView extends View {
|
|||||||
final int action = ev.getAction();
|
final int action = ev.getAction();
|
||||||
|
|
||||||
if (mIsSelectingText) {
|
if (mIsSelectingText) {
|
||||||
int cy = (int) (ev.getY() / mRenderer.mFontLineSpacing) + mTopRow;
|
updateFloatingToolbarVisibility(ev);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
mActionMode.invalidateContentRect();
|
|
||||||
invalidate();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
mGestureRecognizer.onTouchEvent(ev);
|
mGestureRecognizer.onTouchEvent(ev);
|
||||||
return true;
|
return true;
|
||||||
} else if (ev.isFromSource(InputDevice.SOURCE_MOUSE)) {
|
} else if (ev.isFromSource(InputDevice.SOURCE_MOUSE)) {
|
||||||
@@ -561,7 +525,7 @@ public final class TerminalView extends View {
|
|||||||
Log.i(EmulatorDebug.LOG_TAG, "onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")");
|
Log.i(EmulatorDebug.LOG_TAG, "onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")");
|
||||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||||
if (mIsSelectingText) {
|
if (mIsSelectingText) {
|
||||||
toggleSelectingText(null);
|
stopTextSelectionMode();
|
||||||
return true;
|
return true;
|
||||||
} else if (mClient.shouldBackButtonBeMappedToEscape()) {
|
} else if (mClient.shouldBackButtonBeMappedToEscape()) {
|
||||||
// Intercept back button to treat it as escape:
|
// Intercept back button to treat it as escape:
|
||||||
@@ -581,6 +545,7 @@ public final class TerminalView extends View {
|
|||||||
if (LOG_KEY_EVENTS)
|
if (LOG_KEY_EVENTS)
|
||||||
Log.i(EmulatorDebug.LOG_TAG, "onKeyDown(keyCode=" + keyCode + ", isSystem()=" + event.isSystem() + ", event=" + event + ")");
|
Log.i(EmulatorDebug.LOG_TAG, "onKeyDown(keyCode=" + keyCode + ", isSystem()=" + event.isSystem() + ", event=" + event + ")");
|
||||||
if (mEmulator == null) return true;
|
if (mEmulator == null) return true;
|
||||||
|
stopTextSelectionMode();
|
||||||
|
|
||||||
if (mClient.onKeyDown(keyCode, event, mTermSession)) {
|
if (mClient.onKeyDown(keyCode, event, mTermSession)) {
|
||||||
invalidate();
|
invalidate();
|
||||||
@@ -770,59 +735,436 @@ public final class TerminalView extends View {
|
|||||||
} else {
|
} else {
|
||||||
mRenderer.render(mEmulator, canvas, mTopRow, mSelY1, mSelY2, mSelX1, mSelX2);
|
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;
|
SelectionModifierCursorController selectionController = getSelectionController();
|
||||||
int top = (mSelY1 + 1 - mTopRow) * mRenderer.mFontLineSpacing + mRenderer.mFontLineSpacingAndAscent;
|
if (selectionController != null && selectionController.isActive()) {
|
||||||
mLeftSelectionHandle.setBounds(right - gripHandleWidth, top, right, top + mLeftSelectionHandle.getIntrinsicHeight());
|
selectionController.updatePosition();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Toggle text selection mode in the view. */
|
/** Toggle text selection mode in the view. */
|
||||||
@TargetApi(23)
|
@TargetApi(23)
|
||||||
public void toggleSelectingText(MotionEvent ev) {
|
public void startSelectingText(MotionEvent ev) {
|
||||||
mIsSelectingText = !mIsSelectingText;
|
int cx = (int) (ev.getX() / mRenderer.mFontWidth);
|
||||||
mClient.copyModeChanged(mIsSelectingText);
|
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);
|
mHandleHeight = mDrawable.getIntrinsicHeight();
|
||||||
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;
|
mHandleWidth = handleWidth;
|
||||||
mSelY1 = mSelY2 = cy;
|
mTouchOffsetY = -mHandleHeight * 0.3f;
|
||||||
|
mHotspotY = 0;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
TerminalBuffer screen = mEmulator.getScreen();
|
public void changeOrientation(int orientation) {
|
||||||
if (!" ".equals(screen.getSelectedText(mSelX1, mSelY1, mSelX1, mSelY1))) {
|
if (mOrientation != orientation) {
|
||||||
// Selecting something other than whitespace. Expand to word.
|
setOrientation(orientation);
|
||||||
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))) {
|
@Override
|
||||||
mSelX2++;
|
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;
|
final int[] coords = mTempCoords;
|
||||||
mIsDraggingLeftSelection = true;
|
hostView.getLocationInWindow(coords);
|
||||||
mSelectionDownX = ev.getX();
|
final int posX = coords[0] + mPointX;
|
||||||
mSelectionDownY = ev.getY();
|
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() {
|
final ActionMode.Callback callback = new ActionMode.Callback() {
|
||||||
@Override
|
@Override
|
||||||
@@ -864,7 +1206,7 @@ public final class TerminalView extends View {
|
|||||||
showContextMenu();
|
showContextMenu();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
toggleSelectingText(null);
|
stopTextSelectionMode();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -873,7 +1215,6 @@ public final class TerminalView extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
mActionMode = startActionMode(new ActionMode.Callback2() {
|
mActionMode = startActionMode(new ActionMode.Callback2() {
|
||||||
@Override
|
@Override
|
||||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||||
@@ -901,24 +1242,277 @@ public final class TerminalView extends View {
|
|||||||
int x2 = Math.round(mSelX2 * mRenderer.mFontWidth);
|
int x2 = Math.round(mSelX2 * mRenderer.mFontWidth);
|
||||||
int y1 = Math.round((mSelY1 - mTopRow) * mRenderer.mFontLineSpacing);
|
int y1 = Math.round((mSelY1 - mTopRow) * mRenderer.mFontLineSpacing);
|
||||||
int y2 = Math.round((mSelY2 + 1 - mTopRow) * mRenderer.mFontLineSpacing);
|
int y2 = Math.round((mSelY2 + 1 - mTopRow) * mRenderer.mFontLineSpacing);
|
||||||
outRect.set(Math.min(x1, x2), y1, Math.max(x1, x2), y2);
|
|
||||||
|
|
||||||
|
if (x1 > x2) {
|
||||||
|
int tmp = x1;
|
||||||
|
x1 = x2;
|
||||||
|
x2 = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
outRect.set(x1, y1 + mHandleHeight, x2, y2 + mHandleHeight);
|
||||||
}
|
}
|
||||||
}, ActionMode.TYPE_FLOATING);
|
}, ActionMode.TYPE_FLOATING);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
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;
|
mSelX1 = mSelY1 = mSelX2 = mSelY2 = -1;
|
||||||
|
mIsSelectingText = false;
|
||||||
|
|
||||||
|
mClient.copyModeChanged(mIsSelectingText);
|
||||||
|
|
||||||
invalidate();
|
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() {
|
private void showFloatingToolbar() {
|
||||||
return mEmulator.getScreen().getSelectedText(0, mTopRow, mEmulator.mColumns, mTopRow +mEmulator.mRows);
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user