Compare commits

..
16 Commits
Author SHA1 Message Date
Leonid Plyushch 068cb7da50 update gradle wrapper 2019-12-02 17:22:11 +02:00
Leonid Plyushch 919dcfdf0e extra keys: handle actions UP & CANCEL separately
Related issue: https://github.com/termux/termux-app/issues/905
2019-11-24 19:11:14 +01:00
Leonid Plyushch 8a560ebdad extra keys: improve handling of DnD mode
Do not disturb mode is now handled only for SDKs pre-28. Extra keys will
not vibrate only when total silence mode is used.
2019-11-24 19:11:09 +01:00
Fredrik Fornwall 847745a06e Remove outdated tests
Remove tests that asserted that Cursor Down (CUD) and Cursor Up (CUU)
escape sequences were affected by the scrolling region set by DECSTBM.

This was incorrect and recently fixed:
https://github.com/termux/termux-app/issues/1340
2019-11-24 19:10:59 +01:00
Fredrik Fornwall 8413c4d136 Bump version to 0.83 2019-11-24 19:10:30 +01:00
Fredrik Fornwall 58389b506f Do not limit cursor movement to scroll region
The scrolling region set by DECSTBM should not affect the
Cursor Down (CUD) and Cursor Up (CUU) escape sequences.

Fixes #1340.
2019-11-10 22:10:10 +01:00
Fredrik Fornwall d4c0b7cfd0 Bump version to 0.81 2019-11-10 19:15:08 +01:00
Fredrik Fornwall 908fe1ef70 Use updated android 5 bootstrap zips 2019-11-10 19:14:48 +01:00
Fredrik Fornwall 18cb23ec16 Revert "Include bootstrap zips as shared libraries"
This reverts commit e334db0c5b.
2019-11-10 19:09:46 +01:00
Fredrik Fornwall 0e8b83c09f Update Android gradle plug-in 2019-11-10 19:06:28 +01:00
Fredrik Fornwall 465eeb9373 Bump version to 0.79 2019-11-03 22:40:40 +01:00
Fredrik Fornwall a4cf8f6cda Bump version to 0.78 2019-11-03 19:17:37 +01:00
Fredrik Fornwall 4fca04b523 Bump version to 0.77 2019-11-03 19:09:55 +01:00
Leonid Plyushch cd30112feb always set LD_LIBRARY_PATH 2019-10-28 13:41:26 +01:00
Fredrik Fornwall e334db0c5b Include bootstrap zips as shared libraries 2019-10-28 13:41:26 +01:00
Leonid Plyushch 456443acf2 do not allow to backup data by Google
Why:

 * During backup process Termux is being killed in most cases.
 * Backup data is limited to 25 MB.
 * Backup may not be performed/updated in certain cases.
2019-10-28 13:36:48 +02:00
25 changed files with 263 additions and 1046 deletions
-1
View File
@@ -8,7 +8,6 @@ build/
*.so
.externalNativeBuild
.cxx
*.zip
# Crashlytics configuations
com_crashlytics_export_strings.xml
+6 -88
View File
@@ -1,12 +1,10 @@
plugins {
id "com.android.application"
}
apply plugin: 'com.android.application'
android {
compileSdkVersion 28
dependencies {
implementation "androidx.annotation:annotation:1.1.0"
implementation "androidx.annotation:annotation:1.0.1"
implementation "androidx.viewpager:viewpager:1.0.0"
implementation "androidx.drawerlayout:drawerlayout:1.0.0"
implementation project(":terminal-view")
@@ -14,10 +12,10 @@ android {
defaultConfig {
applicationId "com.termux"
minSdkVersion 24
minSdkVersion 21
targetSdkVersion 28
versionCode 90
versionName "0.90"
versionCode 83
versionName "0.83"
externalNativeBuild {
ndkBuild {
@@ -28,7 +26,6 @@ android {
ndk {
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
}
}
signingConfigs {
@@ -56,23 +53,10 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
externalNativeBuild {
ndkBuild {
path "src/main/cpp/Android.mk"
}
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
dependencies {
testImplementation 'junit:junit:4.13'
testImplementation 'org.robolectric:robolectric:4.3'
testImplementation 'junit:junit:4.12'
}
task versionName {
@@ -80,69 +64,3 @@ task versionName {
print android.defaultConfig.versionName
}
}
def downloadBootstrap(String arch, String expectedChecksum, int version) {
def digest = java.security.MessageDigest.getInstance("SHA-256")
def localUrl = "src/main/cpp/bootstrap-" + arch + ".zip"
def file = new File(projectDir, localUrl)
if (file.exists()) {
def buffer = new byte[8192]
def input = new FileInputStream(file)
while (true) {
def readBytes = input.read(buffer)
if (readBytes < 0) break
digest.update(buffer, 0, readBytes)
}
def checksum = new BigInteger(1, digest.digest()).toString(16)
if (checksum == expectedChecksum) {
return
} else {
logger.quiet("Deleting old local file with wrong hash: " + localUrl)
file.delete()
}
}
def remoteUrl = "https://bintray.com/termux/bootstrap/download_file?file_path=bootstrap-" + arch + "-v" + version + ".zip"
logger.quiet("Downloading " + remoteUrl + " ...")
file.parentFile.mkdirs()
def out = new BufferedOutputStream(new FileOutputStream(file))
def connection = new URL(remoteUrl).openConnection()
connection.setInstanceFollowRedirects(true)
def digestStream = new java.security.DigestInputStream(connection.inputStream, digest)
out << digestStream
out.close()
def checksum = new BigInteger(1, digest.digest()).toString(16)
if (checksum != expectedChecksum) {
file.delete()
throw new GradleException("Wrong checksum for " + remoteUrl + ": expected: " + expectedChecksum + ", actual: " + checksum)
}
}
clean {
doLast {
def tree = fileTree(new File(projectDir, 'src/main/cpp'))
tree.include 'bootstrap-*.zip'
tree.each { it.delete() }
}
}
task downloadBootstraps(){
doLast {
def version = 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)
}
}
+6 -6
View File
@@ -14,7 +14,7 @@
<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"/>
<uses-permission-sdk-23 android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<application
android:extractNativeLibs="true"
@@ -31,11 +31,11 @@
<activity
android:name="com.termux.app.TermuxActivity"
android:label="@string/application_name"
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|uiMode|keyboard|keyboardHidden|navigation"
android:label="@string/application_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
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" />
@@ -120,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.samsung.android.multidisplay.keep_process_alive" android:value="true"/>
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
</application>
</manifest>
-5
View File
@@ -1,5 +0,0 @@
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := libtermux-bootstrap
LOCAL_SRC_FILES := termux-bootstrap-zip.S termux-bootstrap.c
include $(BUILD_SHARED_LIBRARY)
-18
View File
@@ -1,18 +0,0 @@
.global blob
.global blob_size
.section .rodata
blob:
#if defined __i686__
.incbin "bootstrap-i686.zip"
#elif defined __x86_64__
.incbin "bootstrap-x86_64.zip"
#elif defined __aarch64__
.incbin "bootstrap-aarch64.zip"
#elif defined __arm__
.incbin "bootstrap-arm.zip"
#else
# error Unsupported arch
#endif
1:
blob_size:
.int 1b - blob
-11
View File
@@ -1,11 +0,0 @@
#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,9 +1,5 @@
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;
@@ -28,11 +24,7 @@ public final class BackgroundJob {
final Process mProcess;
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) {
public BackgroundJob(String cwd, String fileToExecute, final String[] args, final TermuxService service) {
String[] env = buildEnvironment(false, cwd);
if (cwd == null) cwd = TermuxService.HOME_PATH;
@@ -51,28 +43,6 @@ 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
@@ -80,13 +50,11 @@ 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);
@@ -100,28 +68,29 @@ 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) {
@@ -141,7 +110,7 @@ public final class BackgroundJob {
environment.add("TERM=xterm-256color");
environment.add("HOME=" + TermuxService.HOME_PATH);
environment.add("PREFIX=" + TermuxService.PREFIX_PATH);
environment.add("BOOTCLASSPATH=" + System.getenv("BOOTCLASSPATH"));
environment.add("BOOTCLASSPATH" + System.getenv("BOOTCLASSPATH"));
environment.add("ANDROID_ROOT=" + System.getenv("ANDROID_ROOT"));
environment.add("ANDROID_DATA=" + System.getenv("ANDROID_DATA"));
// EXTERNAL_STORAGE is needed for /system/bin/am to work on at least
@@ -154,9 +123,7 @@ public final class BackgroundJob {
// Keep the default path so that system binaries can be used in the failsafe session.
environment.add("PATH= " + System.getenv("PATH"));
} else {
if (shouldAddLdLibraryPath()) {
environment.add("LD_LIBRARY_PATH=" + TermuxService.PREFIX_PATH + "/lib");
}
environment.add("LD_LIBRARY_PATH=" + TermuxService.PREFIX_PATH + "/lib");
environment.add("LANG=en_US.UTF-8");
environment.add("PATH=" + TermuxService.PREFIX_PATH + "/bin:" + TermuxService.PREFIX_PATH + "/bin/applets");
environment.add("PWD=" + cwd);
@@ -166,20 +133,6 @@ public final class BackgroundJob {
return environment.toArray(new String[0]);
}
private static boolean shouldAddLdLibraryPath() {
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("//termux.net stable")) {
return true;
}
}
} catch (IOException e) {
Log.e(LOG_TAG, "Error trying to read sources.list", e);
}
return false;
}
public static int getPid(Process p) {
try {
Field f = p.getClass().getDeclaredField("pid");
@@ -437,7 +437,11 @@ public final class ExtraKeysView extends GridLayout {
LayoutParams param = new GridLayout.LayoutParams();
param.width = 0;
param.height = 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.setMargins(0, 0, 0, 0);
param.columnSpec = GridLayout.spec(col, GridLayout.FILL, 1.f);
param.rowSpec = GridLayout.spec(row, GridLayout.FILL, 1.f);
@@ -188,12 +188,18 @@ 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 (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
return true;
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 {
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUESTCODE_PERMISSION_STORAGE);
return false;
// Always granted before Android 6.0.
return true;
}
}
@@ -603,9 +609,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
new AlertDialog.Builder(this).setTitle(R.string.max_terminals_reached_title).setMessage(R.string.max_terminals_reached_message)
.setPositiveButton(android.R.string.ok, null).show();
} else {
TerminalSession currentSession = getCurrentTermSession();
String workingDirectory = (currentSession == null) ? null : currentSession.getCwd();
TerminalSession newSession = mTermService.createTermSession(null, null, workingDirectory, failSafe);
TerminalSession newSession = mTermService.createTermSession(null, null, null, failSafe);
if (sessionName != null) {
newSession.mSessionName = sessionName;
}
@@ -4,6 +4,7 @@ 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;
@@ -15,12 +16,14 @@ 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;
@@ -35,7 +38,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 zip file is loaded from a shared library.
* (4) The architecture is determined and an appropriate bootstrap zip url is determined in {@link #determineZipUrl()}.
* <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:
@@ -79,8 +82,8 @@ final class TermuxInstaller {
final byte[] buffer = new byte[8096];
final List<Pair<String, String>> symlinks = new ArrayList<>(50);
final byte[] zipBytes = loadZipBytes();
try (ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
final URL zipUrl = determineZipUrl();
try (ZipInputStream zipInput = new ZipInputStream(zipUrl.openStream())) {
ZipEntry zipEntry;
while ((zipEntry = zipInput.getNextEntry()) != null) {
if (zipEntry.getName().equals("SYMLINKS.txt")) {
@@ -164,13 +167,32 @@ final class TermuxInstaller {
}
}
public static byte[] loadZipBytes() {
// Only load the shared library when necessary to save memory usage.
System.loadLibrary("termux-bootstrap");
return getZip();
/** Get bootstrap zip url for this systems cpu architecture. */
private static URL determineZipUrl() throws MalformedURLException {
String archName = determineTermuxArchName();
String url = "https://termux.net/bootstrap/new/bootstrap-" + archName + ".zip";
return new URL(url);
}
public static native byte[] 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));
}
/** Delete a folder and all its content or throw. Don't follow symlinks. */
static void deleteFolder(File fileOrDirectory) throws IOException {
@@ -66,7 +66,6 @@ final class TermuxPreferences {
int mBellBehaviour = BELL_VIBRATE;
boolean mBackIsEscape;
boolean mDisableVolumeVirtualKeys;
boolean mShowExtraKeys;
String[][] mExtraKeys;
@@ -199,7 +198,6 @@ final class TermuxPreferences {
}
mBackIsEscape = "escape".equals(props.getProperty("back-key", "back"));
mDisableVolumeVirtualKeys = "volume".equals(props.getProperty("volume-keys", "virtual"));
shortcuts.clear();
parseAction("shortcut.create-session", SHORTCUT_ACTION_CREATE_SESSION, props);
@@ -114,17 +114,19 @@ 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);
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
String packageName = getPackageName();
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
Intent whitelist = new Intent();
whitelist.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
whitelist.setData(Uri.parse("package:" + packageName));
whitelist.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
startActivity(whitelist);
} catch (ActivityNotFoundException e) {
Log.e(EmulatorDebug.LOG_TAG, "Failed to call ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS", e);
try {
startActivity(whitelist);
} catch (ActivityNotFoundException e) {
Log.e(EmulatorDebug.LOG_TAG, "Failed to call ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS", e);
}
}
}
@@ -148,7 +150,7 @@ public final class TermuxService extends Service implements SessionChangedCallba
String cwd = intent.getStringExtra(EXTRA_CURRENT_WORKING_DIRECTORY);
if (intent.getBooleanExtra(EXTRA_EXECUTE_IN_BACKGROUND, false)) {
BackgroundJob task = new BackgroundJob(cwd, executablePath, arguments, this, intent.getParcelableExtra("pendingIntent"));
BackgroundJob task = new BackgroundJob(cwd, executablePath, arguments, this);
mBackgroundTasks.add(task);
updateNotification();
} else {
@@ -264,9 +264,7 @@ 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 (mActivity.mSettings.mDisableVolumeVirtualKeys) {
return false;
} else if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
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,7 +21,6 @@ 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 {
@@ -37,11 +36,6 @@ 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();
@@ -56,7 +50,7 @@ public class TermuxFileReceiverActivity extends Activity {
final Uri sharedUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
if (sharedText != null) {
if (isSharedTextAnUrl(sharedText)) {
if (Patterns.WEB_URL.matcher(sharedText).matches()) {
handleUrlAndFinish(sharedText);
} else {
String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
@@ -1,27 +1,25 @@
package com.termux.app;
import org.junit.Assert;
import org.junit.Test;
import junit.framework.TestCase;
import java.util.Collections;
import java.util.LinkedHashSet;
public class TermuxActivityTest {
public class TermuxActivityTest extends TestCase {
private void assertUrlsAre(String text, String... urls) {
LinkedHashSet<String> expected = new LinkedHashSet<>();
Collections.addAll(expected, urls);
Assert.assertEquals(expected, TermuxActivity.extractUrls(text));
}
private void assertUrlsAre(String text, String... urls) {
LinkedHashSet<String> expected = new LinkedHashSet<>();
Collections.addAll(expected, urls);
assertEquals(expected, TermuxActivity.extractUrls(text));
}
@Test
public void testExtractUrls() {
assertUrlsAre("hello http://example.com world", "http://example.com");
public void testExtractUrls() {
assertUrlsAre("hello http://example.com world", "http://example.com");
assertUrlsAre("http://example.com\nhttp://another.com", "http://example.com", "http://another.com");
assertUrlsAre("http://example.com\nhttp://another.com", "http://example.com", "http://another.com");
assertUrlsAre("hello http://example.com world and http://more.example.com with secure https://more.example.com",
"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");
}
}
@@ -1,32 +0,0 @@
package com.termux.filepicker;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import java.util.ArrayList;
import java.util.List;
@RunWith(RobolectricTestRunner.class)
public class TermuxFileReceiverActivityTest {
@Test
public void testIsSharedTextAnUrl() {
List<String> validUrls = new ArrayList<>();
validUrls.add("http://example.com");
validUrls.add("https://example.com");
validUrls.add("https://example.com/path/parameter=foo");
validUrls.add("magnet:?xt=urn:btih:d540fc48eb12f2833163eed6421d449dd8f1ce1f&dn=Ubuntu+desktop+19.04+%2864bit%29&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=udp%3A%2F%2Ftracker.publicbt.com%3A80&tr=udp%3A%2F%2Ftracker.ccc.de%3A80");
for (String url : validUrls) {
Assert.assertTrue(TermuxFileReceiverActivity.isSharedTextAnUrl(url));
}
List<String> invalidUrls = new ArrayList<>();
invalidUrls.add("a test with example.com");
for (String url : invalidUrls) {
Assert.assertFalse(TermuxFileReceiverActivity.isSharedTextAnUrl(url));
}
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ buildscript {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.3'
classpath 'com.android.tools.build:gradle:3.5.2'
}
}
Binary file not shown.
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+17 -12
View File
@@ -154,19 +154,19 @@ if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
i=$((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
@@ -175,9 +175,14 @@ 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" "$@"
+1 -1
View File
@@ -60,7 +60,7 @@ tasks.withType(Test) {
}
dependencies {
testImplementation 'junit:junit:4.13'
testImplementation 'junit:junit:4.12'
}
apply from: '../scripts/bintray-publish.gradle'
@@ -1267,7 +1267,6 @@ 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;
@@ -8,7 +8,6 @@ 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;
@@ -340,26 +339,4 @@ 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;
}
}
+2 -2
View File
@@ -20,7 +20,7 @@ android {
compileSdkVersion 28
dependencies {
implementation "androidx.annotation:annotation:1.1.0"
implementation "androidx.annotation:annotation:1.0.1"
api project(":terminal-emulator")
}
@@ -44,7 +44,7 @@ android {
}
dependencies {
testImplementation 'junit:junit:4.13'
testImplementation 'junit:junit:4.12'
}
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.Drawable;
import android.graphics.drawable.BitmapDrawable;
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,16 +25,9 @@ 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;
@@ -42,7 +35,6 @@ 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 {
@@ -62,14 +54,11 @@ public final class TerminalView extends View {
/** The top row of text to display. Ranges from -activeTranscriptRows to 0. */
int mTopRow;
boolean mIsSelectingText = false;
boolean mIsSelectingText = false, mIsDraggingLeftSelection, mInitialTextSelection;
int mSelX1 = -1, mSelX2 = -1, mSelY1 = -1, mSelY2 = -1;
float mSelectionDownX, mSelectionDownY;
private ActionMode mActionMode;
Drawable mSelectHandleLeft;
Drawable mSelectHandleRight;
final int[] mTempCoords = new int[2];
Rect mTempRect;
private SelectionModifierCursorController mSelectionModifierCursorController;
private BitmapDrawable mLeftSelectionHandle, mRightSelectionHandle;
float mScaleFactor = 1.f;
final GestureAndScaleRecognizer mGestureRecognizer;
@@ -113,7 +102,7 @@ public final class TerminalView extends View {
public boolean onSingleTapUp(MotionEvent e) {
if (mEmulator == null) return true;
if (mIsSelectingText) {
stopTextSelectionMode();
toggleSelectingText(null);
return true;
}
requestFocus();
@@ -128,7 +117,7 @@ public final class TerminalView extends View {
@Override
public boolean onScroll(MotionEvent e, float distanceX, float distanceY) {
if (mEmulator == null) return true;
if (mEmulator == null || mIsSelectingText) 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,
@@ -155,7 +144,7 @@ public final class TerminalView extends View {
@Override
public boolean onFling(final MotionEvent e2, float velocityX, float velocityY) {
if (mEmulator == null) return true;
if (mEmulator == null || mIsSelectingText) return true;
// Do not start scrolling until last fling has been taken care of:
if (!mScroller.isFinished()) return true;
@@ -206,7 +195,7 @@ public final class TerminalView extends View {
if (mClient.onLongPress(e)) return;
if (!mIsSelectingText) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
startSelectingText(e);
toggleSelectingText(e);
}
}
});
@@ -298,7 +287,6 @@ 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);
@@ -380,7 +368,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.
stopTextSelectionMode();
toggleSelectingText(null);
} else {
skipScrolling = true;
mTopRow -= rowShift;
@@ -487,7 +475,56 @@ public final class TerminalView extends View {
final int action = ev.getAction();
if (mIsSelectingText) {
updateFloatingToolbarVisibility(ev);
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;
}
mGestureRecognizer.onTouchEvent(ev);
return true;
} else if (ev.isFromSource(InputDevice.SOURCE_MOUSE)) {
@@ -525,7 +562,7 @@ public final class TerminalView extends View {
Log.i(EmulatorDebug.LOG_TAG, "onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")");
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (mIsSelectingText) {
stopTextSelectionMode();
toggleSelectingText(null);
return true;
} else if (mClient.shouldBackButtonBeMappedToEscape()) {
// Intercept back button to treat it as escape:
@@ -545,7 +582,6 @@ 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();
@@ -735,436 +771,59 @@ 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.
SelectionModifierCursorController selectionController = getSelectionController();
if (selectionController != null && selectionController.isActive()) {
selectionController.updatePosition();
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);
}
}
}
/** Toggle text selection mode in the view. */
@TargetApi(23)
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;
public void toggleSelectingText(MotionEvent ev) {
mIsSelectingText = !mIsSelectingText;
mClient.copyModeChanged(mIsSelectingText);
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--;
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);
}
while (mSelX2 < mEmulator.mColumns - 1 && !"".equals(screen.getSelectedText(mSelX2 + 1, mSelY1, mSelX2 + 1, mSelY1))) {
mSelX2++;
}
}
startTextSelectionMode();
}
public TerminalSession getCurrentSession() {
return mTermSession;
}
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;
private CharSequence getText() {
return mEmulator.getScreen().getSelectedText(0, mTopRow, mEmulator.mColumns, mTopRow + mEmulator.mRows);
}
mSelX1 = mSelX2 = cx;
mSelY1 = mSelY2 = cy;
@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;
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--;
}
case RIGHT: {
if (mSelectHandleRight == null) {
mSelectHandleRight = getContext().getDrawable(
R.drawable.text_select_handle_right_material);
}
mDrawable = mSelectHandleRight;
handleWidth = mDrawable.getIntrinsicWidth();
mHotspotX = handleWidth / 4;
break;
}
}
mHandleHeight = mDrawable.getIntrinsicHeight();
mHandleWidth = handleWidth;
mTouchOffsetY = -mHandleHeight * 0.3f;
mHotspotY = 0;
invalidate();
}
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;
}
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();
while (mSelX2 < mEmulator.mColumns - 1 && !"".equals(screen.getSelectedText(mSelX2 + 1, mSelY1, mSelX2 + 1, mSelY1))) {
mSelX2++;
}
}
}
@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();
mInitialTextSelection = true;
mIsDraggingLeftSelection = true;
mSelectionDownX = ev.getX();
mSelectionDownY = ev.getY();
final ActionMode.Callback callback = new ActionMode.Callback() {
@Override
@@ -1206,7 +865,7 @@ public final class TerminalView extends View {
showContextMenu();
break;
}
stopTextSelectionMode();
toggleSelectingText(null);
return true;
}
@@ -1215,304 +874,57 @@ public final class TerminalView extends View {
}
};
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;
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);
}
outRect.set(x1, y1 + mHandleHeight, x2, y2 + mHandleHeight);
}
}, ActionMode.TYPE_FLOATING);
}
public void hide() {
mStartHandle.hide();
mEndHandle.hide();
mIsShowing = false;
if (mActionMode != null) {
// This will hide the mSelectionModifierCursorController
mActionMode.finish();
}
}
public boolean isActive() {
return mIsShowing;
}
public void updatePosition(HandleView handle, int x, int y) {
TerminalBuffer screen = mEmulator.getScreen();
final int scrollRows = screen.getActiveRows() - mEmulator.mRows;
if (handle == mStartHandle) {
mSelX1 = getCursorX(x);
mSelY1 = getCursorY(y);
if (mSelX1 < 0) {
mSelX1 = 0;
}
if (mSelY1 < -scrollRows) {
mSelY1 = -scrollRows;
} else if (mSelY1 > mEmulator.mRows - 1) {
mSelY1 = mEmulator.mRows - 1;
}
if (mSelY1 > mSelY2) {
mSelY1 = mSelY2;
}
if (mSelY1 == mSelY2 && mSelX1 > mSelX2) {
mSelX1 = mSelX2;
}
if (!mEmulator.isAlternateBufferActive()) {
if (mSelY1 <= mTopRow) {
mTopRow--;
if (mTopRow < -scrollRows) {
mTopRow = -scrollRows;
}
} else if (mSelY1 >= mTopRow + mEmulator.mRows) {
mTopRow++;
if (mTopRow > 0) {
mTopRow = 0;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
return callback.onActionItemClicked(mode, item);
}
mSelX1 = getValidCurX(screen, mSelY1, mSelX1);
@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 {
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);
mActionMode = startActionMode(callback);
}
invalidate();
}
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();
} else {
mActionMode.finish();
mSelX1 = mSelY1 = mSelX2 = mSelY2 = -1;
mIsSelectingText = false;
mClient.copyModeChanged(mIsSelectingText);
invalidate();
}
}
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);
}
public TerminalSession getCurrentSession() {
return mTermSession;
}
private void showFloatingToolbar() {
if (mActionMode != null) {
int delay = ViewConfiguration.getDoubleTapTimeout();
postDelayed(mShowFloatingToolbar, delay);
}
private CharSequence getText() {
return mEmulator.getScreen().getSelectedText(0, mTopRow, mEmulator.mColumns, mTopRow +mEmulator.mRows);
}
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();
}
}
}
}