Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
cdccc2c433 | ||
|
|
070436a6ed | ||
|
|
69ded4b33e | ||
|
|
e5a8c0eb4a | ||
|
|
29815fb3f0 | ||
|
|
e930ac08aa | ||
|
|
3b969a0077 | ||
|
|
ae717d8f5f | ||
|
|
6c55c3c1be | ||
|
|
c0a0822843 | ||
|
|
08b08d1c47 | ||
|
|
c76cec186a | ||
|
|
5ba3f7cf6d | ||
|
|
468f878a38 | ||
|
|
c50a367063 | ||
|
|
4189f598b9 | ||
|
|
fdb3764f5c | ||
|
|
cb67e805de | ||
|
|
3f83368f42 | ||
|
|
8c7462678f | ||
|
|
6f11390cc4 | ||
|
|
33d390a228 | ||
|
|
937eb350b2 | ||
|
|
3b4ece6bd8 | ||
|
|
35a4fdacbe | ||
|
|
0332779d6a | ||
|
|
f19575b942 | ||
|
|
6a95809da3 | ||
|
|
ac8dab70ef | ||
|
|
243285f7c2 | ||
|
|
b6182216d5 | ||
|
|
e206121bbc | ||
|
|
deceffad00 | ||
|
|
c19909cef1 | ||
|
|
5b7e40638c | ||
|
|
a3673d1af5 | ||
|
|
d5f9cf85c9 | ||
|
|
549f09573f | ||
|
|
94e5bc86fb | ||
|
|
370ac2bd89 | ||
|
|
7041f41981 | ||
|
|
dd6a21257b | ||
|
|
445da0c4ea | ||
|
|
92980824b1 | ||
|
|
7d42b07a32 | ||
|
|
e502b9169f | ||
|
|
9f2d887ca0 | ||
|
|
e6aacc5f08 | ||
|
|
ff935be54a |
20
.cirrus.yml
Normal file
20
.cirrus.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
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"
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -7,6 +7,8 @@ build/
|
||||
*.apk
|
||||
*.so
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
*.zip
|
||||
|
||||
# Crashlytics configuations
|
||||
com_crashlytics_export_strings.xml
|
||||
|
||||
34
.travis.yml
34
.travis.yml
@@ -1,34 +0,0 @@
|
||||
sudo: false
|
||||
language: android
|
||||
jdk: oraclejdk8
|
||||
|
||||
env:
|
||||
global:
|
||||
# The next declaration is the encrypted COVERITY_SCAN_TOKEN, created
|
||||
# via the "travis encrypt" command using the project repo's public key
|
||||
- secure: "LdajbHNfRlpnqzhX5KY2Vr7KtzU9vXDs1TCNn93J6Dt522f2AaiyUDJvISvz+uslk0WJiS5bB5vGwQmXginxz6Qi6uMgMbjWXulv1vfs6ZviKpUX348DOp1qKPa8WfVNB66F84SwGIfc8cRMAgCFw79l/DFgLErubF8vKo1wZ8Hmvrz//+RJ0BGMa3YRc4VyJhAL0P+0Wc1Q2Im7R9EovAxC5pZXBIMSgr6g5GzLWPisbNLXpMPGsDeYhcenO6XCtCCy+aNxUYM8vcrLDzlVXR5Hy7KEs/MGRTS0Yk13TWUEYa5wBpKelFTszdWYLVn5ANreh/aXRVfHpnW3epotMYguLx1kSvOhWEnc4F+qqv3nle2LpDg9Y9bcLyTTcYnPl9smqEVVjEDu0FoIr1V58xkG4Oc6BPIvLRjlMVU96PXh2HxMLuGsJ/xM+uAFU9oVMbC07xn42Eu5O4NHOHJNOwMWac4/lSKRK8W/7/vWuXj5vhkD9ZsGVpN70UtY5HAfNUGADnTeDblvjgFTNZ2mUN/u0o7Z8ZFURYllZ9YU+Vr2nPf9CAhVBjuwFWx8uRQpAg1aDmc1dVMJijRBeBeU/uWhYqsGp34wkNEl8VGzob4R4QTyI8+T7CndGqKVmbTK/SjqKhjjPpbXIAfOH+JtxvAnNmb8XeQSJ32uK2nexFo="
|
||||
|
||||
android:
|
||||
components:
|
||||
- platform-tools
|
||||
- tools
|
||||
- build-tools-28.0.3
|
||||
- android-28
|
||||
- extra-android-m2repository
|
||||
|
||||
before_install:
|
||||
- yes | sdkmanager "ndk-bundle"
|
||||
- yes | sdkmanager "platforms;android-28"
|
||||
|
||||
script:
|
||||
- ./gradlew testDebugUnitTest
|
||||
|
||||
addons:
|
||||
coverity_scan:
|
||||
project:
|
||||
name: "termux/termux-app"
|
||||
description: "Terminal emulator and Linux environment for Android"
|
||||
notification_email: fredrik@fornwall.net
|
||||
build_command_prepend: "./gradlew clean"
|
||||
build_command: "./gradlew build"
|
||||
branch_pattern: master
|
||||
79
README.md
79
README.md
@@ -1,32 +1,63 @@
|
||||
[](https://travis-ci.org/termux/termux-app)
|
||||
# Termux application
|
||||
|
||||
[](https://cirrus-ci.com/termux/termux-app)
|
||||
[](https://gitter.im/termux/termux)
|
||||
|
||||
[Termux](https://termux.com) is an Android terminal application and Linux environment.
|
||||
|
||||
Termux app
|
||||
==========
|
||||
- [Termux Reddit community](https://reddit.com/r/termux)
|
||||
- [Termux Wiki](https://wiki.termux.com/wiki/)
|
||||
- [Termux Twitter](http://twitter.com/termux/)
|
||||
|
||||
[Termux](https://termux.com) is an Android terminal app and Linux environment.
|
||||
Note that this repository is for the app itself (the user interface and the
|
||||
terminal emulation). For the packages installable inside the app, see
|
||||
[termux/termux-packages](https://github.com/termux/termux-packages)
|
||||
|
||||
* [Termux on Google Play Store](https://play.google.com/store/apps/details?id=com.termux)
|
||||
* [Termux on F-Droid](https://f-droid.org/repository/browse/?fdid=com.termux)
|
||||
* [Termux Google+ community](http://termux.com/community/)
|
||||
* [Termux Wiki](https://wiki.termux.com/wiki/)
|
||||
* [Termux Twitter](http://twitter.com/termux/)
|
||||
## Installation
|
||||
|
||||
Note that this repository is for the app itself (the user interface and the terminal emulation). For the packages installable inside the app, see [termux/termux-packages](https://github.com/termux/termux-packages)
|
||||
Termux:Widget application can be obtained from:
|
||||
|
||||
Terminal resources
|
||||
==================
|
||||
* [XTerm control sequences](http://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
|
||||
* [vt100.net](http://vt100.net/)
|
||||
* [Terminal codes (ANSI and terminfo equivalents)](http://wiki.bash-hackers.org/scripting/terminalcodes)
|
||||
- [Google Play](https://play.google.com/store/apps/details?id=com.termux)
|
||||
- [F-Droid](https://f-droid.org/en/packages/com.termux/)
|
||||
- [Kali Nethunter Store](https://store.nethunter.com/en/packages/com.termux/)
|
||||
|
||||
Terminal emulators
|
||||
==================
|
||||
* VTE (libvte): Terminal emulator widget for GTK+, mainly used in gnome-terminal. [Source](https://github.com/GNOME/vte), [Open Issues](https://bugzilla.gnome.org/buglist.cgi?quicksearch=product%3A%22vte%22+), and [All (including closed) issues](https://bugzilla.gnome.org/buglist.cgi?bug_status=RESOLVED&bug_status=VERIFIED&chfield=resolution&chfieldfrom=-2000d&chfieldvalue=FIXED&product=vte&resolution=FIXED).
|
||||
* iTerm 2: OS X terminal application. [Source](https://github.com/gnachman/iTerm2), [Issues](https://gitlab.com/gnachman/iterm2/issues) and [Documentation](http://www.iterm2.com/documentation.html) (which includes [iTerm2 proprietary escape codes](http://www.iterm2.com/documentation-escape-codes.html)).
|
||||
* Konsole: KDE terminal application. [Source](https://projects.kde.org/projects/kde/applications/konsole/repository), in particular [tests](https://projects.kde.org/projects/kde/applications/konsole/repository/revisions/master/show/tests), [Bugs](https://bugs.kde.org/buglist.cgi?bug_severity=critical&bug_severity=grave&bug_severity=major&bug_severity=crash&bug_severity=normal&bug_severity=minor&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole) and [Wishes](https://bugs.kde.org/buglist.cgi?bug_severity=wishlist&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole).
|
||||
* hterm: JavaScript terminal implementation from Chromium. [Source](https://github.com/chromium/hterm), including [tests](https://github.com/chromium/hterm/blob/master/js/hterm_vt_tests.js), and [Google group](https://groups.google.com/a/chromium.org/forum/#!forum/chromium-hterm).
|
||||
* xterm: The grandfather of terminal emulators. [Source](http://invisible-island.net/datafiles/release/xterm.tar.gz).
|
||||
* Connectbot: Android SSH client. [Source](https://github.com/connectbot/connectbot)
|
||||
* Android Terminal Emulator: Android terminal app which Termux terminal handling is based on. Inactive. [Source](https://github.com/jackpal/Android-Terminal-Emulator).
|
||||
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).
|
||||
|
||||
Signature keys of all offered builds are different. Before you switch the
|
||||
installation source, you will have to uninstall the Termux application and
|
||||
all currently installed plugins.
|
||||
|
||||
## Terminal resources
|
||||
|
||||
- [XTerm control sequences](http://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
|
||||
- [vt100.net](http://vt100.net/)
|
||||
- [Terminal codes (ANSI and terminfo equivalents)](http://wiki.bash-hackers.org/scripting/terminalcodes)
|
||||
|
||||
## Terminal emulators
|
||||
|
||||
- VTE (libvte): Terminal emulator widget for GTK+, mainly used in gnome-terminal.
|
||||
[Source](https://github.com/GNOME/vte), [Open Issues](https://bugzilla.gnome.org/buglist.cgi?quicksearch=product%3A%22vte%22+),
|
||||
and [All (including closed) issues](https://bugzilla.gnome.org/buglist.cgi?bug_status=RESOLVED&bug_status=VERIFIED&chfield=resolution&chfieldfrom=-2000d&chfieldvalue=FIXED&product=vte&resolution=FIXED).
|
||||
|
||||
- iTerm 2: OS X terminal application. [Source](https://github.com/gnachman/iTerm2),
|
||||
[Issues](https://gitlab.com/gnachman/iterm2/issues) and [Documentation](http://www.iterm2.com/documentation.html)
|
||||
(which includes [iTerm2 proprietary escape codes](http://www.iterm2.com/documentation-escape-codes.html)).
|
||||
|
||||
- Konsole: KDE terminal application. [Source](https://projects.kde.org/projects/kde/applications/konsole/repository),
|
||||
in particular [tests](https://projects.kde.org/projects/kde/applications/konsole/repository/revisions/master/show/tests),
|
||||
[Bugs](https://bugs.kde.org/buglist.cgi?bug_severity=critical&bug_severity=grave&bug_severity=major&bug_severity=crash&bug_severity=normal&bug_severity=minor&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole)
|
||||
and [Wishes](https://bugs.kde.org/buglist.cgi?bug_severity=wishlist&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole).
|
||||
|
||||
- hterm: JavaScript terminal implementation from Chromium. [Source](https://github.com/chromium/hterm),
|
||||
including [tests](https://github.com/chromium/hterm/blob/master/js/hterm_vt_tests.js),
|
||||
and [Google group](https://groups.google.com/a/chromium.org/forum/#!forum/chromium-hterm).
|
||||
|
||||
- xterm: The grandfather of terminal emulators.
|
||||
[Source](http://invisible-island.net/datafiles/release/xterm.tar.gz).
|
||||
|
||||
- Connectbot: Android SSH client. [Source](https://github.com/connectbot/connectbot)
|
||||
|
||||
- Android Terminal Emulator: Android terminal app which Termux terminal handling
|
||||
is based on. Inactive. [Source](https://github.com/jackpal/Android-Terminal-Emulator).
|
||||
|
||||
123
app/build.gradle
123
app/build.gradle
@@ -1,10 +1,12 @@
|
||||
apply plugin: 'com.android.application'
|
||||
plugins {
|
||||
id "com.android.application"
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
|
||||
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 +14,30 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.termux"
|
||||
minSdkVersion 21
|
||||
minSdkVersion 24
|
||||
targetSdkVersion 28
|
||||
versionCode 72
|
||||
versionName "0.72"
|
||||
versionCode 90
|
||||
versionName "0.90"
|
||||
|
||||
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 {
|
||||
debug {
|
||||
storeFile file('dev_keystore.jks')
|
||||
keyAlias 'alias'
|
||||
storePassword 'xrj45yWGLbsO7W0v'
|
||||
keyPassword 'xrj45yWGLbsO7W0v'
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -24,14 +46,103 @@ android {
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
|
||||
debug {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
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'
|
||||
}
|
||||
|
||||
task versionName {
|
||||
doLast {
|
||||
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 = 18
|
||||
downloadBootstrap("aarch64", "1a4c08a696d452b58f69102428239ec0c07521c0ca9f48b23ef70ae0e5e3d4f8", version)
|
||||
downloadBootstrap("arm", "bff11f2c7e9c1055a22fc5f20bb7507b75f6034e0f5d591ec6725b3407981b85", version)
|
||||
downloadBootstrap("i686", "6fb93020db2807337d82a1537e24612400cacbd10cf4bccaeb0714d51e653da1", version)
|
||||
downloadBootstrap("x86_64", "a6067e5decc486dcad190c1ed9e15366c798e5e7d9b9b9ee6b4b8231290524c3", version)
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
android.applicationVariants.all { variant ->
|
||||
variant.javaCompileProvider.get().dependsOn(downloadBootstraps)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BIN
app/dev_keystore.jks
Normal file
BIN
app/dev_keystore.jks
Normal file
Binary file not shown.
@@ -8,16 +8,17 @@
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<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 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"
|
||||
@@ -30,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" />
|
||||
@@ -119,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
app/src/main/cpp/Android.mk
Normal file
5
app/src/main/cpp/Android.mk
Normal 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
app/src/main/cpp/termux-bootstrap-zip.S
Normal file
18
app/src/main/cpp/termux-bootstrap-zip.S
Normal 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
app/src/main/cpp/termux-bootstrap.c
Normal file
11
app/src/main/cpp/termux-bootstrap.c
Normal 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,35 @@ 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) {
|
||||
String value = System.getenv(name);
|
||||
if (value != null) {
|
||||
environment.add(name + "=" + value);
|
||||
}
|
||||
}
|
||||
|
||||
static String[] buildEnvironment(boolean failSafe, String cwd) {
|
||||
@@ -103,17 +141,15 @@ 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
|
||||
// Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3.
|
||||
environment.add("EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE"));
|
||||
String androidRuntimeRoot = System.getenv("ANDROID_RUNTIME_ROOT");
|
||||
// ANDROID_RUNTIME_ROOT is required for `am` to run on Android Q
|
||||
if (androidRuntimeRoot != null) {
|
||||
environment.add("ANDROID_RUNTIME_ROOT=" + androidRuntimeRoot);
|
||||
}
|
||||
// ANDROID_RUNTIME_ROOT and ANDROID_TZDATA_ROOT are required for `am` to run on Android Q
|
||||
addToEnvIfPresent(environment, "ANDROID_RUNTIME_ROOT");
|
||||
addToEnvIfPresent(environment, "ANDROID_TZDATA_ROOT");
|
||||
if (failSafe) {
|
||||
// Keep the default path so that system binaries can be used in the failsafe session.
|
||||
environment.add("PATH= " + System.getenv("PATH"));
|
||||
@@ -134,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) {
|
||||
|
||||
@@ -10,7 +10,7 @@ public class BellUtil {
|
||||
private static BellUtil instance = null;
|
||||
private static final Object lock = new Object();
|
||||
|
||||
public static BellUtil with(Context context) {
|
||||
public static BellUtil getInstance(Context context) {
|
||||
if (instance == null) {
|
||||
synchronized (lock) {
|
||||
if (instance == null) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.termux.app;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
@@ -73,6 +74,18 @@ public final class ExtraKeysView extends GridLayout {
|
||||
put("RIGHT", KeyEvent.KEYCODE_DPAD_RIGHT);
|
||||
put("DOWN", KeyEvent.KEYCODE_DPAD_DOWN);
|
||||
put("ENTER", KeyEvent.KEYCODE_ENTER);
|
||||
put("F1", KeyEvent.KEYCODE_F1);
|
||||
put("F2", KeyEvent.KEYCODE_F2);
|
||||
put("F3", KeyEvent.KEYCODE_F3);
|
||||
put("F4", KeyEvent.KEYCODE_F4);
|
||||
put("F5", KeyEvent.KEYCODE_F5);
|
||||
put("F6", KeyEvent.KEYCODE_F6);
|
||||
put("F7", KeyEvent.KEYCODE_F7);
|
||||
put("F8", KeyEvent.KEYCODE_F8);
|
||||
put("F9", KeyEvent.KEYCODE_F9);
|
||||
put("F10", KeyEvent.KEYCODE_F10);
|
||||
put("F11", KeyEvent.KEYCODE_F11);
|
||||
put("F12", KeyEvent.KEYCODE_F12);
|
||||
}};
|
||||
|
||||
static void sendKey(View view, String keyName) {
|
||||
@@ -338,7 +351,19 @@ public final class ExtraKeysView extends GridLayout {
|
||||
|
||||
final Button finalButton = button;
|
||||
button.setOnClickListener(v -> {
|
||||
finalButton.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||
if (Settings.System.getInt(getContext().getContentResolver(),
|
||||
Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) != 0) {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
View root = getRootView();
|
||||
if(Arrays.asList("CTRL", "ALT", "FN").contains(buttonText)) {
|
||||
ToggleButton self = (ToggleButton) finalButton;
|
||||
@@ -380,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();
|
||||
@@ -406,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);
|
||||
|
||||
@@ -24,7 +24,6 @@ import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.Vibrator;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
@@ -128,6 +127,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
*/
|
||||
boolean mIsVisible;
|
||||
|
||||
boolean mIsUsingBlackUI;
|
||||
|
||||
final SoundPool mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
|
||||
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
|
||||
@@ -187,28 +188,35 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
mSettings = new TermuxPreferences(this);
|
||||
mIsUsingBlackUI = mSettings.isUsingBlackUI();
|
||||
if (mIsUsingBlackUI) {
|
||||
this.setTheme(R.style.Theme_Termux_Black);
|
||||
} else {
|
||||
this.setTheme(R.style.Theme_Termux);
|
||||
}
|
||||
|
||||
super.onCreate(bundle);
|
||||
|
||||
mSettings = new TermuxPreferences(this);
|
||||
|
||||
setContentView(R.layout.drawer_layout);
|
||||
|
||||
if (mIsUsingBlackUI) {
|
||||
findViewById(R.id.left_drawer).setBackgroundColor(
|
||||
getResources().getColor(android.R.color.background_dark)
|
||||
);
|
||||
}
|
||||
|
||||
mTerminalView = findViewById(R.id.terminal_view);
|
||||
mTerminalView.setOnKeyListener(new TermuxViewClient(this));
|
||||
|
||||
@@ -402,7 +410,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
|
||||
break;
|
||||
case TermuxPreferences.BELL_VIBRATE:
|
||||
BellUtil.with(TermuxActivity.this).doBell();
|
||||
BellUtil.getInstance(TermuxActivity.this).doBell();
|
||||
break;
|
||||
case TermuxPreferences.BELL_IGNORE:
|
||||
// Ignore the bell character.
|
||||
@@ -435,7 +443,11 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
boolean sessionRunning = sessionAtRow.isRunning();
|
||||
|
||||
TextView firstLineView = row.findViewById(R.id.row_line);
|
||||
|
||||
if (mIsUsingBlackUI) {
|
||||
firstLineView.setBackground(
|
||||
getResources().getDrawable(R.drawable.selected_session_background_black)
|
||||
);
|
||||
}
|
||||
String name = sessionAtRow.mSessionName;
|
||||
String sessionTitle = sessionAtRow.getTitle();
|
||||
|
||||
@@ -455,7 +467,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
} else {
|
||||
firstLineView.setPaintFlags(firstLineView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
|
||||
}
|
||||
int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? Color.BLACK : Color.RED;
|
||||
int defaultColor = mIsUsingBlackUI ? Color.WHITE : Color.BLACK;
|
||||
int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? defaultColor : Color.RED;
|
||||
firstLineView.setTextColor(color);
|
||||
return row;
|
||||
}
|
||||
@@ -590,8 +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 {
|
||||
String executablePath = (failSafe ? "/system/bin/sh" : null);
|
||||
TerminalSession newSession = mTermService.createTermSession(executablePath, 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;
|
||||
}
|
||||
@@ -656,19 +670,86 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
}
|
||||
|
||||
static LinkedHashSet<CharSequence> extractUrls(String text) {
|
||||
// Pattern for recognizing a URL, based off RFC 3986
|
||||
// http://stackoverflow.com/questions/5713558/detect-and-extract-url-from-a-string
|
||||
|
||||
StringBuilder regex_sb = new StringBuilder();
|
||||
|
||||
regex_sb.append("("); // Begin first matching group.
|
||||
regex_sb.append("(?:"); // Begin scheme group.
|
||||
regex_sb.append("dav|"); // The DAV proto.
|
||||
regex_sb.append("dict|"); // The DICT proto.
|
||||
regex_sb.append("dns|"); // The DNS proto.
|
||||
regex_sb.append("file|"); // File path.
|
||||
regex_sb.append("finger|"); // The Finger proto.
|
||||
regex_sb.append("ftp(?:s?)|"); // The FTP proto.
|
||||
regex_sb.append("git|"); // The Git proto.
|
||||
regex_sb.append("gopher|"); // The Gopher proto.
|
||||
regex_sb.append("http(?:s?)|"); // The HTTP proto.
|
||||
regex_sb.append("imap(?:s?)|"); // The IMAP proto.
|
||||
regex_sb.append("irc(?:[6s]?)|"); // The IRC proto.
|
||||
regex_sb.append("ip[fn]s|"); // The IPFS proto.
|
||||
regex_sb.append("ldap(?:s?)|"); // The LDAP proto.
|
||||
regex_sb.append("pop3(?:s?)|"); // The POP3 proto.
|
||||
regex_sb.append("redis(?:s?)|"); // The Redis proto.
|
||||
regex_sb.append("rsync|"); // The Rsync proto.
|
||||
regex_sb.append("rtsp(?:[su]?)|"); // The RTSP proto.
|
||||
regex_sb.append("sftp|"); // The SFTP proto.
|
||||
regex_sb.append("smb(?:s?)|"); // The SAMBA proto.
|
||||
regex_sb.append("smtp(?:s?)|"); // The SMTP proto.
|
||||
regex_sb.append("svn(?:(?:\\+ssh)?)|"); // The Subversion proto.
|
||||
regex_sb.append("tcp|"); // The TCP proto.
|
||||
regex_sb.append("telnet|"); // The Telnet proto.
|
||||
regex_sb.append("tftp|"); // The TFTP proto.
|
||||
regex_sb.append("udp|"); // The UDP proto.
|
||||
regex_sb.append("vnc|"); // The VNC proto.
|
||||
regex_sb.append("ws(?:s?)"); // The Websocket proto.
|
||||
regex_sb.append(")://"); // End scheme group.
|
||||
regex_sb.append(")"); // End first matching group.
|
||||
|
||||
|
||||
// Begin second matching group.
|
||||
regex_sb.append("(");
|
||||
|
||||
// User name and/or password in format 'user:pass@'.
|
||||
regex_sb.append("(?:\\S+(?::\\S*)?@)?");
|
||||
|
||||
// Begin host group.
|
||||
regex_sb.append("(?:");
|
||||
|
||||
// IP address (from http://www.regular-expressions.info/examples.html).
|
||||
regex_sb.append("(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|");
|
||||
|
||||
// Host name or domain.
|
||||
regex_sb.append("(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)(?:(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))?|");
|
||||
|
||||
// Just path. Used in case of 'file://' scheme.
|
||||
regex_sb.append("/(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)");
|
||||
|
||||
// End host group.
|
||||
regex_sb.append(")");
|
||||
|
||||
// Port number.
|
||||
regex_sb.append("(?::\\d{1,5})?");
|
||||
|
||||
// Resource path with optional query string.
|
||||
regex_sb.append("(?:/[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
|
||||
|
||||
// End second matching group.
|
||||
regex_sb.append(")");
|
||||
|
||||
final Pattern urlPattern = Pattern.compile(
|
||||
"(?:^|[\\W])((ht|f)tp(s?)://|www\\.)" + "(([\\w\\-]+\\.)+?([\\w\\-.~]+/?)*" + "[\\p{Alnum}.,%_=?&#\\-+()\\[\\]*$~@!:/{};']*)",
|
||||
regex_sb.toString(),
|
||||
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
|
||||
|
||||
LinkedHashSet<CharSequence> urlSet = new LinkedHashSet<>();
|
||||
Matcher matcher = urlPattern.matcher(text);
|
||||
|
||||
while (matcher.find()) {
|
||||
int matchStart = matcher.start(1);
|
||||
int matchEnd = matcher.end();
|
||||
String url = text.substring(matchStart, matchEnd);
|
||||
urlSet.add(url);
|
||||
}
|
||||
|
||||
return urlSet;
|
||||
}
|
||||
|
||||
@@ -723,7 +804,18 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
if (session != null) {
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(Intent.EXTRA_TEXT, session.getEmulator().getScreen().getTranscriptText().trim());
|
||||
String transcriptText = session.getEmulator().getScreen().getTranscriptTextWithoutJoinedLines().trim();
|
||||
// See https://github.com/termux/termux-app/issues/1166.
|
||||
final int MAX_LENGTH = 100_000;
|
||||
if (transcriptText.length() > MAX_LENGTH) {
|
||||
int cutOffIndex = transcriptText.length() - MAX_LENGTH;
|
||||
int nextNewlineIndex = transcriptText.indexOf('\n', cutOffIndex);
|
||||
if (nextNewlineIndex != -1 && nextNewlineIndex != transcriptText.length() - 1) {
|
||||
cutOffIndex = nextNewlineIndex + 1;
|
||||
}
|
||||
transcriptText = transcriptText.substring(cutOffIndex).trim();
|
||||
}
|
||||
intent.putExtra(Intent.EXTRA_TEXT, transcriptText);
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_transcript_title));
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.share_transcript_chooser_title)));
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -58,6 +58,7 @@ final class TermuxPreferences {
|
||||
private static final String CURRENT_SESSION_KEY = "current_session";
|
||||
private static final String SCREEN_ALWAYS_ON_KEY = "screen_always_on";
|
||||
|
||||
private String mUseDarkUI;
|
||||
private boolean mScreenAlwaysOn;
|
||||
private int mFontSize;
|
||||
|
||||
@@ -65,6 +66,7 @@ final class TermuxPreferences {
|
||||
int mBellBehaviour = BELL_VIBRATE;
|
||||
|
||||
boolean mBackIsEscape;
|
||||
boolean mDisableVolumeVirtualKeys;
|
||||
boolean mShowExtraKeys;
|
||||
|
||||
String[][] mExtraKeys;
|
||||
@@ -126,6 +128,10 @@ final class TermuxPreferences {
|
||||
return mScreenAlwaysOn;
|
||||
}
|
||||
|
||||
boolean isUsingBlackUI() {
|
||||
return mUseDarkUI.toLowerCase().equals("true");
|
||||
}
|
||||
|
||||
void setScreenAlwaysOn(Context context, boolean newValue) {
|
||||
mScreenAlwaysOn = newValue;
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SCREEN_ALWAYS_ON_KEY, newValue).apply();
|
||||
@@ -173,6 +179,8 @@ final class TermuxPreferences {
|
||||
break;
|
||||
}
|
||||
|
||||
mUseDarkUI = props.getProperty("use-black-ui", "false");
|
||||
|
||||
try {
|
||||
JSONArray arr = new JSONArray(props.getProperty("extra-keys", "[['ESC', 'TAB', 'CTRL', 'ALT', '-', 'DOWN', 'UP']]"));
|
||||
|
||||
@@ -191,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);
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
@@ -16,6 +17,7 @@ import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.PowerManager;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.widget.ArrayAdapter;
|
||||
|
||||
@@ -112,6 +114,20 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
||||
mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, EmulatorDebug.LOG_TAG);
|
||||
mWifiLock.acquire();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
updateNotification();
|
||||
}
|
||||
} else if (ACTION_UNLOCK_WAKE.equals(action)) {
|
||||
@@ -132,11 +148,12 @@ 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 {
|
||||
TerminalSession newSession = createTermSession(executablePath, arguments, cwd, false);
|
||||
boolean failsafe = intent.getBooleanExtra(TermuxActivity.TERMUX_FAILSAFE_SESSION_ACTION, false);
|
||||
TerminalSession newSession = createTermSession(executablePath, arguments, cwd, failsafe);
|
||||
|
||||
// Transform executable path to session name, e.g. "/bin/do-something.sh" => "do something.sh".
|
||||
if (executablePath != null) {
|
||||
@@ -243,7 +260,7 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
||||
try {
|
||||
TermuxInstaller.deleteFolder(termuxTmpDir.getCanonicalFile());
|
||||
} catch (Exception e) {
|
||||
Log.e(EmulatorDebug.LOG_TAG, "Error while removing directory " + termuxTmpDir.getAbsolutePath(), e);
|
||||
Log.e(EmulatorDebug.LOG_TAG, "Error while removing file at " + termuxTmpDir.getAbsolutePath(), e);
|
||||
}
|
||||
|
||||
termuxTmpDir.mkdirs();
|
||||
@@ -271,11 +288,13 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
||||
boolean isLoginShell = false;
|
||||
|
||||
if (executablePath == null) {
|
||||
for (String shellBinary : new String[]{"login", "bash", "zsh"}) {
|
||||
File shellFile = new File(PREFIX_PATH + "/bin/" + shellBinary);
|
||||
if (shellFile.canExecute()) {
|
||||
executablePath = shellFile.getAbsolutePath();
|
||||
break;
|
||||
if (!failSafe) {
|
||||
for (String shellBinary : new String[]{"login", "bash", "zsh"}) {
|
||||
File shellFile = new File(PREFIX_PATH + "/bin/" + shellBinary);
|
||||
if (shellFile.canExecute()) {
|
||||
executablePath = shellFile.getAbsolutePath();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<solid android:color="#E0E0E0" />
|
||||
</shape>
|
||||
</shape>
|
||||
|
||||
4
app/src/main/res/drawable/current_session_black.xml
Normal file
4
app/src/main/res/drawable/current_session_black.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<solid android:color="#212325" />
|
||||
</shape>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_activated="true" android:drawable="@drawable/current_session_black"/>
|
||||
<item android:state_activated="false" android:drawable="@drawable/session_ripple_black"/>
|
||||
</selector>
|
||||
@@ -4,4 +4,4 @@
|
||||
<item>
|
||||
<color android:color="@android:color/white" />
|
||||
</item>
|
||||
</ripple>
|
||||
</ripple>
|
||||
|
||||
7
app/src/main/res/drawable/session_ripple_black.xml
Normal file
7
app/src/main/res/drawable/session_ripple_black.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="@android:color/darker_gray" >
|
||||
<item>
|
||||
<color android:color="@android:color/background_dark" />
|
||||
</item>
|
||||
</ripple>
|
||||
@@ -1,8 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- See https://developer.android.com/training/material/theme.html for how to customize the Material theme. -->
|
||||
<!-- NOTE: Cannot use "Light." since it hides the terminal scrollbar on the default black background. -->
|
||||
<style name="Theme.Termux" parent="@android:style/Theme.Material.Light.NoActionBar">
|
||||
<item name="android:statusBarColor">#000000</item>
|
||||
<item name="android:colorPrimary">#FF000000</item>
|
||||
@@ -23,9 +21,29 @@
|
||||
<item name="android:windowAllowEnterTransitionOverlap">true</item>
|
||||
</style>
|
||||
|
||||
<style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">
|
||||
<!-- Seen in buttons on alert dialog: -->
|
||||
<style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">
|
||||
<!-- Seen in buttons on alert dialog: -->
|
||||
<item name="android:colorAccent">#212121</item>
|
||||
</style>
|
||||
|
||||
<!-- See https://developer.android.com/training/material/theme.html for how to customize the Material theme. -->
|
||||
<!-- NOTE: Cannot use "Light." since it hides the terminal scrollbar on the default black background. -->
|
||||
<style name="Theme.Termux.Black" parent="@android:style/Theme.Material.NoActionBar">
|
||||
<item name="android:statusBarColor">#000000</item>
|
||||
<item name="android:colorPrimary">#FF000000</item>
|
||||
<item name="android:windowBackground">@android:color/black</item>
|
||||
|
||||
<!-- Seen in buttons on left drawer: -->
|
||||
<item name="android:colorAccent">#FDFDFD</item>
|
||||
<!-- Avoid action mode toolbar pushing down terminal content when
|
||||
selecting text on pre-6.0 (non-floating toolbar). -->
|
||||
<item name="android:windowActionModeOverlay">true</item>
|
||||
|
||||
<item name="android:windowTranslucentStatus">true</item>
|
||||
<item name="android:windowTranslucentNavigation">true</item>
|
||||
|
||||
<!-- https://developer.android.com/training/tv/start/start.html#transition-color -->
|
||||
<item name="android:windowAllowReturnTransitionOverlap">true</item>
|
||||
<item name="android:windowAllowEnterTransitionOverlap">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -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,27 @@
|
||||
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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.4.0'
|
||||
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.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
51
gradlew
vendored
51
gradlew
vendored
@@ -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" "$@"
|
||||
|
||||
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
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0
|
||||
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.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m"
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
@@ -60,7 +60,7 @@ tasks.withType(Test) {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'junit:junit:4.13'
|
||||
}
|
||||
|
||||
apply from: '../scripts/bintray-publish.gradle'
|
||||
|
||||
@@ -41,7 +41,15 @@ public final class TerminalBuffer {
|
||||
return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows).trim();
|
||||
}
|
||||
|
||||
public String getTranscriptTextWithoutJoinedLines() {
|
||||
return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows, false).trim();
|
||||
}
|
||||
|
||||
public String getSelectedText(int selX1, int selY1, int selX2, int selY2) {
|
||||
return getSelectedText(selX1, selY1, selX2, selY2, true);
|
||||
}
|
||||
|
||||
public String getSelectedText(int selX1, int selY1, int selX2, int selY2, boolean joinBackLines) {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
final int columns = mColumns;
|
||||
|
||||
@@ -79,7 +87,8 @@ public final class TerminalBuffer {
|
||||
}
|
||||
if (lastPrintingCharIndex != -1)
|
||||
builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1);
|
||||
if (!rowLineWrap && row < selY2 && row < mScreenRows - 1) builder.append('\n');
|
||||
if ((!joinBackLines || !rowLineWrap)
|
||||
&& row < selY2 && row < mScreenRows - 1) builder.append('\n');
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user