Compare commits
93 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89048274dd | ||
|
|
d3b4d35b2a | ||
|
|
4080f41cc7 | ||
|
|
b4e2c99d46 | ||
|
|
c5923201a4 | ||
|
|
bafd21bb39 | ||
|
|
0f20fab02c | ||
|
|
c5ae5bb06a | ||
|
|
bbd46a763c | ||
|
|
dc145d65f8 | ||
|
|
ce2d1c0d88 | ||
|
|
f2f7f963e6 | ||
|
|
2e53ef038e | ||
|
|
8c82f43dce | ||
|
|
3a16f461e7 | ||
|
|
594a5dfe25 | ||
|
|
d5e007dbb3 | ||
|
|
f4335d3824 | ||
|
|
90b6f93697 | ||
|
|
17c4a45212 | ||
|
|
6d9ffb6922 | ||
|
|
8472fce8ba | ||
|
|
69d954a583 | ||
|
|
ec1087d56f | ||
|
|
be6a73d862 | ||
|
|
80c81b274d | ||
|
|
cd9dbac548 | ||
|
|
0c837796f0 | ||
|
|
3417e37e8d | ||
|
|
31ba36e0fa | ||
|
|
c444f1fd28 | ||
|
|
9f36ed06b8 | ||
|
|
c2ab5bcd50 | ||
|
|
48fab33b79 | ||
|
|
8d7a67645b | ||
|
|
186b49d429 | ||
|
|
40a2775f52 | ||
|
|
f802d6001d | ||
|
|
50f66a12da | ||
|
|
d8e6fd21d1 | ||
|
|
0964d83572 | ||
|
|
cee0466dd7 | ||
|
|
de4f334e24 | ||
|
|
ab205ae05b | ||
|
|
b29b24f507 | ||
|
|
b3472e9e62 | ||
|
|
ab59e08959 | ||
|
|
8b566485e8 | ||
|
|
231c02a0c7 | ||
|
|
65f30e77ba | ||
|
|
686677ae45 | ||
|
|
85037a75a6 | ||
|
|
6025afc2c0 | ||
|
|
a4e4f76775 | ||
|
|
02af113dda | ||
|
|
fc4ef838bf | ||
|
|
86bd3ca21b | ||
|
|
367398bafb | ||
|
|
dd502e55f8 | ||
|
|
798125ef7a | ||
|
|
5126f06e06 | ||
|
|
0bdbf314ef | ||
|
|
2deb9899bd | ||
|
|
694ccc38c4 | ||
|
|
c949940374 | ||
|
|
d09f70de1e | ||
|
|
ac338ce2c5 | ||
|
|
092a83a688 | ||
|
|
3533b13de8 | ||
|
|
d68a0f05be | ||
|
|
9e5293a08e | ||
|
|
3f04a0a0d5 | ||
|
|
e558f824c5 | ||
|
|
8ff72ddd8b | ||
|
|
3e05356a69 | ||
|
|
82673d3f82 | ||
|
|
f2757fdb76 | ||
|
|
81fb669d0e | ||
|
|
4a45b1b617 | ||
|
|
62b08b3cf1 | ||
|
|
1e83ea3151 | ||
|
|
9c42fdb3d6 | ||
|
|
42e3163e92 | ||
|
|
68912139f6 | ||
|
|
edc92bbcbb | ||
|
|
d7520642de | ||
|
|
adae111d5c | ||
|
|
dc9272790b | ||
|
|
05ea2ea238 | ||
|
|
ad293562bc | ||
|
|
f9a565d1e0 | ||
|
|
5b3909cb73 | ||
|
|
1be5139253 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -33,6 +33,8 @@ local.properties
|
||||
.idea/scopes/scope_settings.xml
|
||||
.idea/vcs.xml
|
||||
.idea/dictionaries/
|
||||
.idea/caches/
|
||||
.idea/codeStyles/
|
||||
*.iml
|
||||
|
||||
# OS-specific files
|
||||
|
||||
@@ -12,13 +12,14 @@ android:
|
||||
components:
|
||||
- platform-tools
|
||||
- tools
|
||||
- build-tools-25.0.3
|
||||
- android-25
|
||||
- build-tools-27.0.3
|
||||
- android-27
|
||||
- extra-android-m2repository
|
||||
|
||||
before_install:
|
||||
- git clone https://github.com/urho3d/android-ndk.git $HOME/android-ndk
|
||||
- export ANDROID_NDK_HOME=$HOME/android-ndk
|
||||
- yes | sdkmanager "platforms;android-27"
|
||||
|
||||
script:
|
||||
- ./gradlew testDebugUnitTest
|
||||
|
||||
@@ -7,8 +7,11 @@ Termux app
|
||||
|
||||
* [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 Help](http://termux.com/help/)
|
||||
* [Termux Facebook](https://facebook.com/termux/)
|
||||
* [Termux Google+ community](http://termux.com/community/)
|
||||
* [Termux Help](http://termux.com/help/)
|
||||
* [Termux Twitter](http://twitter.com/termux/)
|
||||
* [Termux Wiki](https://wiki.termux.com/wiki/)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 25
|
||||
buildToolsVersion "25.0.3"
|
||||
compileSdkVersion 27
|
||||
|
||||
dependencies {
|
||||
compile 'com.android.support:support-annotations:25.3.1'
|
||||
compile "com.android.support:support-v4:25.3.1"
|
||||
compile project(":terminal-view")
|
||||
implementation 'com.android.support:support-annotations:27.1.1'
|
||||
implementation "com.android.support:support-core-ui:27.1.1"
|
||||
implementation project(":terminal-view")
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.termux"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 25
|
||||
versionCode 52
|
||||
versionName "0.52"
|
||||
targetSdkVersion 27
|
||||
versionCode 63
|
||||
versionName "0.63"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -28,5 +27,5 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testCompile 'junit:junit:4.12'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
}
|
||||
|
||||
10
app/proguard-rules.pro
vendored
10
app/proguard-rules.pro
vendored
@@ -7,11 +7,5 @@
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
-renamesourcefileattribute SourceFile
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.termux"
|
||||
android:installLocation="internalOnly"
|
||||
android:sharedUserId="com.termux"
|
||||
@@ -23,10 +24,15 @@
|
||||
android:theme="@style/Theme.Termux"
|
||||
android:supportsRtl="false" >
|
||||
|
||||
<!-- This (or rather, value 2.1 or higher) is needed to make the Samsung Galaxy S8
|
||||
mark the app with "This app is optimized to run in full screen." -->
|
||||
<meta-data android:name="android.max_aspect" android:value="10.0" />
|
||||
|
||||
<activity
|
||||
android:name="com.termux.app.TermuxActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:launchMode="singleTask"
|
||||
android:resizeableActivity="true"
|
||||
android:windowSoftInputMode="adjustResize|stateAlwaysVisible" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
@@ -44,6 +50,7 @@
|
||||
android:exported="false"
|
||||
android:theme="@android:style/Theme.Material.Light.DarkActionBar"
|
||||
android:parentActivityName=".app.TermuxActivity"
|
||||
android:resizeableActivity="true"
|
||||
android:label="@string/application_name" />
|
||||
|
||||
<activity
|
||||
@@ -51,6 +58,7 @@
|
||||
android:label="@string/application_name"
|
||||
android:taskAffinity="com.termux.filereceiver"
|
||||
android:excludeFromRecents="true"
|
||||
android:resizeableActivity="true"
|
||||
android:noHistory="true">
|
||||
<!-- Accept multiple file types when sending. -->
|
||||
<intent-filter>
|
||||
@@ -65,7 +73,7 @@
|
||||
<data android:mimeType="video/*" />
|
||||
</intent-filter>
|
||||
<!-- Be more restrictive for viewing files, restricting ourselves to text files. -->
|
||||
<intent-filter>
|
||||
<intent-filter tools:ignore="AppLinkUrlError">
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/*" />
|
||||
@@ -76,6 +84,18 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity-alias
|
||||
android:name=".HomeActivity"
|
||||
android:targetActivity="com.termux.app.TermuxActivity">
|
||||
|
||||
<!-- Launch activity automatically on boot on Android Things devices -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.IOT_LAUNCHER"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<provider
|
||||
android:name=".filepicker.TermuxDocumentsProvider"
|
||||
android:authorities="com.termux.documents"
|
||||
|
||||
@@ -111,14 +111,13 @@ public final class BackgroundJob {
|
||||
final String pathEnv = "PATH=" + System.getenv("PATH");
|
||||
return new String[]{termEnv, homeEnv, prefixEnv, androidRootEnv, androidDataEnv, pathEnv, externalStorageEnv};
|
||||
} else {
|
||||
final String ps1Env = "PS1=$ ";
|
||||
final String ldEnv = "LD_LIBRARY_PATH=" + TermuxService.PREFIX_PATH + "/lib";
|
||||
final String langEnv = "LANG=en_US.UTF-8";
|
||||
final String pathEnv = "PATH=" + TermuxService.PREFIX_PATH + "/bin:" + TermuxService.PREFIX_PATH + "/bin/applets";
|
||||
final String pwdEnv = "PWD=" + cwd;
|
||||
final String tmpdirEnv = "TMPDIR=" + TermuxService.PREFIX_PATH + "/tmp";
|
||||
|
||||
return new String[]{termEnv, homeEnv, prefixEnv, ps1Env, ldEnv, langEnv, pathEnv, pwdEnv, androidRootEnv, androidDataEnv, externalStorageEnv, tmpdirEnv};
|
||||
return new String[]{termEnv, homeEnv, prefixEnv, ldEnv, langEnv, pathEnv, pwdEnv, androidRootEnv, androidDataEnv, externalStorageEnv, tmpdirEnv};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,19 @@ package com.termux.app;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
import android.view.Gravity;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.GridLayout;
|
||||
import android.widget.PopupWindow;
|
||||
import android.widget.ToggleButton;
|
||||
|
||||
import com.termux.R;
|
||||
@@ -21,6 +28,8 @@ import com.termux.view.TerminalView;
|
||||
public final class ExtraKeysView extends GridLayout {
|
||||
|
||||
private static final int TEXT_COLOR = 0xFFFFFFFF;
|
||||
private static final int BUTTON_COLOR = 0xFF000000;
|
||||
private static final int BUTTON_PRESSED_COLOR = 0xFF888888;
|
||||
|
||||
public ExtraKeysView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
@@ -38,16 +47,28 @@ public final class ExtraKeysView extends GridLayout {
|
||||
case "TAB":
|
||||
keyCode = KeyEvent.KEYCODE_TAB;
|
||||
break;
|
||||
case "▲":
|
||||
case "HOME":
|
||||
keyCode = KeyEvent.KEYCODE_MOVE_HOME;
|
||||
break;
|
||||
case "END":
|
||||
keyCode = KeyEvent.KEYCODE_MOVE_END;
|
||||
break;
|
||||
case "PGUP":
|
||||
keyCode = KeyEvent.KEYCODE_PAGE_UP;
|
||||
break;
|
||||
case "PGDN":
|
||||
keyCode = KeyEvent.KEYCODE_PAGE_DOWN;
|
||||
break;
|
||||
case "↑":
|
||||
keyCode = KeyEvent.KEYCODE_DPAD_UP;
|
||||
break;
|
||||
case "◀":
|
||||
case "←":
|
||||
keyCode = KeyEvent.KEYCODE_DPAD_LEFT;
|
||||
break;
|
||||
case "▶":
|
||||
case "→":
|
||||
keyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
|
||||
break;
|
||||
case "▼":
|
||||
case "↓":
|
||||
keyCode = KeyEvent.KEYCODE_DPAD_DOWN;
|
||||
break;
|
||||
case "―":
|
||||
@@ -57,11 +78,11 @@ public final class ExtraKeysView extends GridLayout {
|
||||
chars = keyName;
|
||||
}
|
||||
|
||||
TerminalView terminalView = view.findViewById(R.id.terminal_view);
|
||||
if (keyCode > 0) {
|
||||
view.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
|
||||
view.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
|
||||
terminalView.onKeyDown(keyCode, new KeyEvent(KeyEvent.ACTION_UP, keyCode));
|
||||
// view.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
|
||||
} else {
|
||||
TerminalView terminalView = (TerminalView) view.findViewById(R.id.terminal_view);
|
||||
TerminalSession session = terminalView.getCurrentSession();
|
||||
if (session != null) session.write(chars);
|
||||
}
|
||||
@@ -70,6 +91,9 @@ public final class ExtraKeysView extends GridLayout {
|
||||
private ToggleButton controlButton;
|
||||
private ToggleButton altButton;
|
||||
private ToggleButton fnButton;
|
||||
private ScheduledExecutorService scheduledExecutor;
|
||||
private PopupWindow popupWindow;
|
||||
private int longPressCount;
|
||||
|
||||
public boolean readControlButton() {
|
||||
if (controlButton.isPressed()) return true;
|
||||
@@ -101,24 +125,47 @@ public final class ExtraKeysView extends GridLayout {
|
||||
return result;
|
||||
}
|
||||
|
||||
void popup(View view, String text) {
|
||||
int width = view.getMeasuredWidth();
|
||||
int height = view.getMeasuredHeight();
|
||||
Button button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||
button.setText(text);
|
||||
button.setTextColor(TEXT_COLOR);
|
||||
button.setPadding(0, 0, 0, 0);
|
||||
button.setMinHeight(0);
|
||||
button.setMinWidth(0);
|
||||
button.setMinimumWidth(0);
|
||||
button.setMinimumHeight(0);
|
||||
button.setWidth(width);
|
||||
button.setHeight(height);
|
||||
button.setBackgroundColor(BUTTON_PRESSED_COLOR);
|
||||
popupWindow = new PopupWindow(this);
|
||||
popupWindow.setWidth(LayoutParams.WRAP_CONTENT);
|
||||
popupWindow.setHeight(LayoutParams.WRAP_CONTENT);
|
||||
popupWindow.setContentView(button);
|
||||
popupWindow.setOutsideTouchable(true);
|
||||
popupWindow.setFocusable(false);
|
||||
popupWindow.showAsDropDown(view, 0, -2 * height);
|
||||
}
|
||||
|
||||
void reload() {
|
||||
altButton = controlButton = null;
|
||||
removeAllViews();
|
||||
|
||||
String[][] buttons = {
|
||||
{"ESC", "CTRL", "ALT", "TAB", "―", "/", "|"}
|
||||
{"ESC", "/", "―", "HOME", "↑", "END", "PGUP"},
|
||||
{"TAB", "CTRL", "ALT", "←", "↓", "→", "PGDN"}
|
||||
};
|
||||
|
||||
final int rows = buttons.length;
|
||||
final int cols = buttons[0].length;
|
||||
final int[] cols = {buttons[0].length, buttons[1].length};
|
||||
|
||||
setRowCount(rows);
|
||||
setColumnCount(cols);
|
||||
setColumnCount(cols[0]);
|
||||
|
||||
for (int row = 0; row < rows; row++) {
|
||||
for (int col = 0; col < cols; col++) {
|
||||
for (int col = 0; col < cols[row]; col++) {
|
||||
final String buttonText = buttons[row][col];
|
||||
|
||||
Button button;
|
||||
switch (buttonText) {
|
||||
case "CTRL":
|
||||
@@ -140,6 +187,7 @@ public final class ExtraKeysView extends GridLayout {
|
||||
|
||||
button.setText(buttonText);
|
||||
button.setTextColor(TEXT_COLOR);
|
||||
button.setPadding(0, 0, 0, 0);
|
||||
|
||||
final Button finalButton = button;
|
||||
button.setOnClickListener(new OnClickListener() {
|
||||
@@ -162,12 +210,69 @@ public final class ExtraKeysView extends GridLayout {
|
||||
}
|
||||
});
|
||||
|
||||
GridLayout.LayoutParams param = new GridLayout.LayoutParams();
|
||||
param.height = param.width = 0;
|
||||
param.rightMargin = param.topMargin = 0;
|
||||
button.setOnTouchListener(new OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
final View root = getRootView();
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
longPressCount = 0;
|
||||
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
|
||||
if ("↑↓←→".contains(buttonText)) {
|
||||
scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||
scheduledExecutor.scheduleWithFixedDelay(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
longPressCount++;
|
||||
sendKey(root, buttonText);
|
||||
}
|
||||
}, 400, 80, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
return true;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if ("―/".contains(buttonText)) {
|
||||
if (popupWindow == null && event.getY() < 0) {
|
||||
v.setBackgroundColor(BUTTON_COLOR);
|
||||
String text = "―".equals(buttonText) ? "|" : "\\";
|
||||
popup(v, text);
|
||||
}
|
||||
if (popupWindow != null && event.getY() > 0) {
|
||||
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
|
||||
popupWindow.dismiss();
|
||||
popupWindow = null;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
v.setBackgroundColor(BUTTON_COLOR);
|
||||
if (scheduledExecutor != null) {
|
||||
scheduledExecutor.shutdownNow();
|
||||
scheduledExecutor = null;
|
||||
}
|
||||
if (longPressCount == 0) {
|
||||
if (popupWindow != null && "―/".contains(buttonText)) {
|
||||
popupWindow.setContentView(null);
|
||||
popupWindow.dismiss();
|
||||
popupWindow = null;
|
||||
sendKey(root, "―".equals(buttonText) ? "|" : "\\");
|
||||
} else {
|
||||
v.performClick();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
LayoutParams param = new GridLayout.LayoutParams();
|
||||
param.width = param.height = 0;
|
||||
param.setMargins(0, 0, 0, 0);
|
||||
param.setGravity(Gravity.LEFT);
|
||||
float weight = "▲▼◀▶".contains(buttonText) ? 0.7f : 1.f;
|
||||
param.columnSpec = GridLayout.spec(col, GridLayout.FILL, weight);
|
||||
param.columnSpec = GridLayout.spec(col, GridLayout.FILL, 1.f);
|
||||
param.rowSpec = GridLayout.spec(row, GridLayout.FILL, 1.f);
|
||||
button.setLayoutParams(param);
|
||||
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
package com.termux.app;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
|
||||
import com.termux.R;
|
||||
|
||||
/**
|
||||
* Utility to manage full screen immersive mode.
|
||||
* <p/>
|
||||
* See https://code.google.com/p/android/issues/detail?id=5497
|
||||
*/
|
||||
final class FullScreenHelper {
|
||||
|
||||
private boolean mEnabled = false;
|
||||
final TermuxActivity mActivity;
|
||||
|
||||
public FullScreenHelper(TermuxActivity activity) {
|
||||
this.mActivity = activity;
|
||||
}
|
||||
|
||||
public void setImmersive(boolean enabled) {
|
||||
if (enabled == mEnabled) return;
|
||||
mEnabled = enabled;
|
||||
|
||||
View decorView = mActivity.getWindow().getDecorView();
|
||||
|
||||
if (enabled) {
|
||||
decorView.setOnSystemUiVisibilityChangeListener
|
||||
(new View.OnSystemUiVisibilityChangeListener() {
|
||||
@Override
|
||||
public void onSystemUiVisibilityChange(int visibility) {
|
||||
if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
|
||||
if (mActivity.mSettings.isShowExtraKeys()) {
|
||||
mActivity.findViewById(R.id.viewpager).setVisibility(View.VISIBLE);
|
||||
}
|
||||
setImmersiveMode();
|
||||
} else {
|
||||
mActivity.findViewById(R.id.viewpager).setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
});
|
||||
setImmersiveMode();
|
||||
} else {
|
||||
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
|
||||
decorView.setOnSystemUiVisibilityChangeListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isColorLight(int color) {
|
||||
double darkness = 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255;
|
||||
return darkness < 0.5;
|
||||
}
|
||||
|
||||
void setImmersiveMode() {
|
||||
int flags = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
| View.SYSTEM_UI_FLAG_FULLSCREEN;
|
||||
int color = ((ColorDrawable) mActivity.getWindow().getDecorView().getBackground()).getColor();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && isColorLight(color))
|
||||
flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
|
||||
mActivity.getWindow().getDecorView().setSystemUiVisibility(flags);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -73,6 +73,7 @@ import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -95,7 +96,6 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
private static final int CONTEXTMENU_KILL_PROCESS_ID = 4;
|
||||
private static final int CONTEXTMENU_RESET_TERMINAL_ID = 5;
|
||||
private static final int CONTEXTMENU_STYLING_ID = 6;
|
||||
private static final int CONTEXTMENU_TOGGLE_FULLSCREEN_ID = 7;
|
||||
private static final int CONTEXTMENU_HELP_ID = 8;
|
||||
|
||||
private static final int MAX_SESSIONS = 8;
|
||||
@@ -111,8 +111,6 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
|
||||
ExtraKeysView mExtraKeysView;
|
||||
|
||||
final FullScreenHelper mFullScreenHelper = new FullScreenHelper(this);
|
||||
|
||||
TermuxPreferences mSettings;
|
||||
|
||||
/**
|
||||
@@ -157,7 +155,6 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
|
||||
void checkForFontAndColors() {
|
||||
try {
|
||||
// Hard-coded paths since this file is used also in Termux:Float.
|
||||
@SuppressLint("SdCardPath") File fontFile = new File("/data/data/com.termux/files/home/.termux/font.ttf");
|
||||
@SuppressLint("SdCardPath") File colorsFile = new File("/data/data/com.termux/files/home/.termux/colors.properties");
|
||||
|
||||
@@ -212,14 +209,13 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
mSettings = new TermuxPreferences(this);
|
||||
|
||||
setContentView(R.layout.drawer_layout);
|
||||
mTerminalView = (TerminalView) findViewById(R.id.terminal_view);
|
||||
mTerminalView = findViewById(R.id.terminal_view);
|
||||
mTerminalView.setOnKeyListener(new TermuxViewClient(this));
|
||||
|
||||
mTerminalView.setTextSize(mSettings.getFontSize());
|
||||
mFullScreenHelper.setImmersive(mSettings.isFullScreen());
|
||||
mTerminalView.requestFocus();
|
||||
|
||||
final ViewPager viewPager = (ViewPager) findViewById(R.id.viewpager);
|
||||
final ViewPager viewPager = findViewById(R.id.viewpager);
|
||||
if (mSettings.isShowExtraKeys()) viewPager.setVisibility(View.VISIBLE);
|
||||
|
||||
viewPager.setAdapter(new PagerAdapter() {
|
||||
@@ -229,19 +225,20 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isViewFromObject(View view, Object object) {
|
||||
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
|
||||
return view == object;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Object instantiateItem(ViewGroup collection, int position) {
|
||||
public Object instantiateItem(@NonNull ViewGroup collection, int position) {
|
||||
LayoutInflater inflater = LayoutInflater.from(TermuxActivity.this);
|
||||
View layout;
|
||||
if (position == 0) {
|
||||
layout = mExtraKeysView = (ExtraKeysView) inflater.inflate(R.layout.extra_keys_main, collection, false);
|
||||
} else {
|
||||
layout = inflater.inflate(R.layout.extra_keys_right, collection, false);
|
||||
final EditText editText = (EditText) layout.findViewById(R.id.text_input);
|
||||
final EditText editText = layout.findViewById(R.id.text_input);
|
||||
editText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||
@Override
|
||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
||||
@@ -265,7 +262,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(ViewGroup collection, int position, Object view) {
|
||||
public void destroyItem(@NonNull ViewGroup collection, int position, @NonNull Object view) {
|
||||
collection.removeView((View) view);
|
||||
}
|
||||
});
|
||||
@@ -276,7 +273,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
if (position == 0) {
|
||||
mTerminalView.requestFocus();
|
||||
} else {
|
||||
final EditText editText = (EditText) viewPager.findViewById(R.id.text_input);
|
||||
final EditText editText = viewPager.findViewById(R.id.text_input);
|
||||
if (editText != null) editText.requestFocus();
|
||||
}
|
||||
}
|
||||
@@ -340,7 +337,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
}
|
||||
|
||||
void toggleShowExtraKeys() {
|
||||
final ViewPager viewPager = (ViewPager) findViewById(R.id.viewpager);
|
||||
final ViewPager viewPager = findViewById(R.id.viewpager);
|
||||
final boolean showNow = mSettings.toggleShowExtraKeys(TermuxActivity.this);
|
||||
viewPager.setVisibility(showNow ? View.VISIBLE : View.GONE);
|
||||
if (showNow && viewPager.getCurrentItem() == 1) {
|
||||
@@ -425,7 +422,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
}
|
||||
};
|
||||
|
||||
ListView listView = (ListView) findViewById(R.id.left_drawer_list);
|
||||
ListView listView = findViewById(R.id.left_drawer_list);
|
||||
mListViewAdapter = new ArrayAdapter<TerminalSession>(getApplicationContext(), R.layout.line_in_drawer, mTermService.getSessions()) {
|
||||
final StyleSpan boldSpan = new StyleSpan(Typeface.BOLD);
|
||||
final StyleSpan italicSpan = new StyleSpan(Typeface.ITALIC);
|
||||
@@ -442,7 +439,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
TerminalSession sessionAtRow = getItem(position);
|
||||
boolean sessionRunning = sessionAtRow.isRunning();
|
||||
|
||||
TextView firstLineView = (TextView) row.findViewById(R.id.row_line);
|
||||
TextView firstLineView = row.findViewById(R.id.row_line);
|
||||
|
||||
String name = sessionAtRow.mSessionName;
|
||||
String sessionTitle = sessionAtRow.getTitle();
|
||||
@@ -538,10 +535,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
if (mTermService != null) {
|
||||
// Respect being stopped from the TermuxService notification action.
|
||||
finish();
|
||||
}
|
||||
// Respect being stopped from the TermuxService notification action.
|
||||
finish();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -645,7 +640,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
final int indexOfSession = mTermService.getSessions().indexOf(session);
|
||||
showToast(toToastTitle(session), false);
|
||||
mListViewAdapter.notifyDataSetChanged();
|
||||
final ListView lv = ((ListView) findViewById(R.id.left_drawer_list));
|
||||
final ListView lv = findViewById(R.id.left_drawer_list);
|
||||
lv.setItemChecked(indexOfSession, true);
|
||||
lv.smoothScrollToPosition(indexOfSession);
|
||||
}
|
||||
@@ -659,7 +654,6 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
menu.add(Menu.NONE, CONTEXTMENU_SHARE_TRANSCRIPT_ID, Menu.NONE, R.string.select_all_and_share);
|
||||
menu.add(Menu.NONE, CONTEXTMENU_RESET_TERMINAL_ID, Menu.NONE, R.string.reset_terminal);
|
||||
menu.add(Menu.NONE, CONTEXTMENU_KILL_PROCESS_ID, Menu.NONE, getResources().getString(R.string.kill_process, getCurrentTermSession().getPid())).setEnabled(currentSession.isRunning());
|
||||
menu.add(Menu.NONE, CONTEXTMENU_TOGGLE_FULLSCREEN_ID, Menu.NONE, R.string.toggle_fullscreen).setCheckable(true).setChecked(mSettings.isFullScreen());
|
||||
menu.add(Menu.NONE, CONTEXTMENU_STYLING_ID, Menu.NONE, R.string.style_terminal);
|
||||
menu.add(Menu.NONE, CONTEXTMENU_HELP_ID, Menu.NONE, R.string.help);
|
||||
}
|
||||
@@ -793,11 +787,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
}
|
||||
}).setNegativeButton(android.R.string.cancel, null).show();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
case CONTEXTMENU_TOGGLE_FULLSCREEN_ID:
|
||||
toggleImmersive();
|
||||
return true;
|
||||
}
|
||||
case CONTEXTMENU_HELP_ID:
|
||||
startActivity(new Intent(this, TermuxHelpActivity.class));
|
||||
return true;
|
||||
@@ -813,12 +804,6 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
}
|
||||
}
|
||||
|
||||
void toggleImmersive() {
|
||||
boolean newValue = !mSettings.isFullScreen();
|
||||
mSettings.setFullScreen(this, newValue);
|
||||
mFullScreenHelper.setImmersive(newValue);
|
||||
}
|
||||
|
||||
void changeFontSize(boolean increase) {
|
||||
mSettings.changeFontSize(this, increase);
|
||||
mTerminalView.setTextSize(mSettings.getFontSize());
|
||||
@@ -837,9 +822,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
public TerminalSession getStoredCurrentSessionOrLast() {
|
||||
TerminalSession stored = TermuxPreferences.getCurrentSession(this);
|
||||
if (stored != null) return stored;
|
||||
int numberOfSessions = mTermService.getSessions().size();
|
||||
if (numberOfSessions == 0) return null;
|
||||
return mTermService.getSessions().get(numberOfSessions - 1);
|
||||
List<TerminalSession> sessions = mTermService.getSessions();
|
||||
return sessions.isEmpty() ? null : sessions.get(sessions.size() - 1);
|
||||
}
|
||||
|
||||
/** Show a toast and dismiss the last one if still visible. */
|
||||
|
||||
@@ -39,7 +39,7 @@ public final class TermuxHelpActivity extends Activity {
|
||||
mWebView.setWebViewClient(new WebViewClient() {
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
if (url.startsWith("https://termux.com")) {
|
||||
if (url.startsWith("https://wiki.termux.com")) {
|
||||
// Inline help.
|
||||
setContentView(progressLayout);
|
||||
return false;
|
||||
@@ -60,7 +60,7 @@ public final class TermuxHelpActivity extends Activity {
|
||||
setContentView(mWebView);
|
||||
}
|
||||
});
|
||||
mWebView.loadUrl("https://termux.com/help.html");
|
||||
mWebView.loadUrl("https://wiki.termux.com/wiki/Main_Page");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.termux.terminal.EmulatorDebug;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
@@ -43,7 +44,7 @@ import java.util.zip.ZipInputStream;
|
||||
* (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
|
||||
* continously encountering zip file entries:
|
||||
* continuously encountering zip file entries:
|
||||
* <p/>
|
||||
* (5.1) If the zip entry encountered is SYMLINKS.txt, go through it and remember all symlinks to setup.
|
||||
* <p/>
|
||||
@@ -209,11 +210,15 @@ final class TermuxInstaller {
|
||||
}
|
||||
|
||||
/** Delete a folder and all its content or throw. */
|
||||
static void deleteFolder(File fileOrDirectory) {
|
||||
static void deleteFolder(File fileOrDirectory) throws IOException {
|
||||
File[] children = fileOrDirectory.listFiles();
|
||||
if (children != null) {
|
||||
for (File child : children) {
|
||||
deleteFolder(child);
|
||||
if (child.getCanonicalFile().equals(child.getAbsoluteFile())) {
|
||||
deleteFolder(child);
|
||||
} else {
|
||||
child.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!fileOrDirectory.delete()) {
|
||||
@@ -228,9 +233,13 @@ final class TermuxInstaller {
|
||||
try {
|
||||
File storageDir = new File(TermuxService.HOME_PATH, "storage");
|
||||
|
||||
if (storageDir.exists() && !storageDir.delete()) {
|
||||
Log.e(LOG_TAG, "Could not delete old $HOME/storage");
|
||||
return;
|
||||
if (storageDir.exists()) {
|
||||
try {
|
||||
deleteFolder(storageDir);
|
||||
} catch (IOException e) {
|
||||
Log.e(LOG_TAG, "Could not delete old $HOME/storage, " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!storageDir.mkdirs()) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.MediaStore;
|
||||
import android.support.annotation.NonNull;
|
||||
@@ -18,6 +19,7 @@ import com.termux.terminal.EmulatorDebug;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
|
||||
public class TermuxOpenReceiver extends BroadcastReceiver {
|
||||
|
||||
@@ -171,6 +173,16 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
|
||||
File file = new File(uri.getPath());
|
||||
try {
|
||||
String path = file.getCanonicalPath();
|
||||
String storagePath = Environment.getExternalStorageDirectory().getCanonicalPath();
|
||||
// See https://support.google.com/faqs/answer/7496913:
|
||||
if (!(path.startsWith(TermuxService.FILES_PATH) || path.startsWith(storagePath))) {
|
||||
throw new IllegalArgumentException("Invalid path: " + path);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,12 +32,10 @@ final class TermuxPreferences {
|
||||
private final int MIN_FONTSIZE;
|
||||
private static final int MAX_FONTSIZE = 256;
|
||||
|
||||
private static final String FULLSCREEN_KEY = "fullscreen";
|
||||
private static final String SHOW_EXTRA_KEYS_KEY = "show_extra_keys";
|
||||
private static final String FONTSIZE_KEY = "fontsize";
|
||||
private static final String CURRENT_SESSION_KEY = "current_session";
|
||||
|
||||
private boolean mFullScreen;
|
||||
private int mFontSize;
|
||||
|
||||
@AsciiBellBehaviour
|
||||
@@ -56,7 +54,6 @@ final class TermuxPreferences {
|
||||
// to prevent invisible text due to zoom be mistake:
|
||||
MIN_FONTSIZE = (int) (4f * dipInPixels);
|
||||
|
||||
mFullScreen = prefs.getBoolean(FULLSCREEN_KEY, false);
|
||||
mShowExtraKeys = prefs.getBoolean(SHOW_EXTRA_KEYS_KEY, false);
|
||||
|
||||
// http://www.google.com/design/spec/style/typography.html#typography-line-height
|
||||
@@ -72,15 +69,6 @@ final class TermuxPreferences {
|
||||
mFontSize = Math.max(MIN_FONTSIZE, Math.min(mFontSize, MAX_FONTSIZE));
|
||||
}
|
||||
|
||||
boolean isFullScreen() {
|
||||
return mFullScreen;
|
||||
}
|
||||
|
||||
void setFullScreen(Context context, boolean newValue) {
|
||||
mFullScreen = newValue;
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(FULLSCREEN_KEY, newValue).apply();
|
||||
}
|
||||
|
||||
boolean isShowExtraKeys() {
|
||||
return mShowExtraKeys;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.termux.app;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
@@ -11,10 +12,10 @@ import android.content.res.Resources;
|
||||
import android.net.Uri;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.PowerManager;
|
||||
import android.support.v4.content.WakefulBroadcastReceiver;
|
||||
import android.util.Log;
|
||||
import android.widget.ArrayAdapter;
|
||||
|
||||
@@ -41,6 +42,8 @@ import java.util.List;
|
||||
*/
|
||||
public final class TermuxService extends Service implements SessionChangedCallback {
|
||||
|
||||
private static final String NOTIFICATION_CHANNEL_ID = "termux_notification_channel";
|
||||
|
||||
/** Note that this is a symlink on the Android M preview. */
|
||||
@SuppressLint("SdCardPath")
|
||||
public static final String FILES_PATH = "/data/data/com.termux/files";
|
||||
@@ -153,11 +156,6 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
||||
Log.e(EmulatorDebug.LOG_TAG, "Unknown TermuxService action: '" + action + "'");
|
||||
}
|
||||
|
||||
if ((flags & START_FLAG_REDELIVERY) == 0) {
|
||||
// Service is started by WBR, not restarted by system, so release the WakeLock from WBR.
|
||||
WakefulBroadcastReceiver.completeWakefulIntent(intent);
|
||||
}
|
||||
|
||||
// If this service really do get killed, there is no point restarting it automatically - let the user do on next
|
||||
// start of {@link Term):
|
||||
return Service.START_NOT_STICKY;
|
||||
@@ -170,6 +168,7 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
setupNotificationChannel();
|
||||
startForeground(NOTIFICATION_ID, buildNotification());
|
||||
}
|
||||
|
||||
@@ -208,8 +207,8 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
||||
builder.setOngoing(true);
|
||||
|
||||
// If holding a wake or wifi lock consider the notification of high priority since it's using power,
|
||||
// otherwise use a minimal priority since this is just a background service notification:
|
||||
builder.setPriority((wakeLockHeld) ? Notification.PRIORITY_HIGH : Notification.PRIORITY_MIN);
|
||||
// otherwise use a low priority
|
||||
builder.setPriority((wakeLockHeld) ? Notification.PRIORITY_HIGH : Notification.PRIORITY_LOW);
|
||||
|
||||
// No need to show a timestamp:
|
||||
builder.setShowWhen(false);
|
||||
@@ -217,6 +216,10 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
||||
// Background color for small notification icon:
|
||||
builder.setColor(0xFF000000);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
builder.setChannelId(NOTIFICATION_CHANNEL_ID);
|
||||
}
|
||||
|
||||
Resources res = getResources();
|
||||
Intent exitIntent = new Intent(this, TermuxService.class).setAction(ACTION_STOP_SERVICE);
|
||||
builder.addAction(android.R.drawable.ic_delete, res.getString(R.string.notification_action_exit), PendingIntent.getService(this, 0, exitIntent, 0));
|
||||
@@ -241,7 +244,6 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
||||
|
||||
for (int i = 0; i < mTerminalSessions.size(); i++)
|
||||
mTerminalSessions.get(i).finishIfRunning();
|
||||
mTerminalSessions.clear();
|
||||
}
|
||||
|
||||
public List<TerminalSession> getSessions() {
|
||||
@@ -341,4 +343,17 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
|
||||
|
||||
String channelName = "Termux";
|
||||
String channelDescription = "Notifications from Termux";
|
||||
int importance = NotificationManager.IMPORTANCE_LOW;
|
||||
|
||||
NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName,importance);
|
||||
channel.setDescription(channelDescription);
|
||||
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
manager.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,8 +73,6 @@ public final class TermuxViewClient implements TerminalViewClient {
|
||||
mActivity.getDrawer().openDrawer(Gravity.LEFT);
|
||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
|
||||
mActivity.getDrawer().closeDrawers();
|
||||
} else if (unicodeChar == 'f'/* full screen */) {
|
||||
mActivity.toggleImmersive();
|
||||
} else if (unicodeChar == 'k'/* keyboard */) {
|
||||
InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
|
||||
|
||||
@@ -195,7 +195,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
||||
final String name = file.getName();
|
||||
final int lastDot = name.lastIndexOf('.');
|
||||
if (lastDot >= 0) {
|
||||
final String extension = name.substring(lastDot + 1);
|
||||
final String extension = name.substring(lastDot + 1).toLowerCase();
|
||||
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||
if (mime != null) return mime;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<!-- Screen border. -->
|
||||
<path android:fillColor="#00000000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeColor="#FFF"
|
||||
android:strokeWidth="3"
|
||||
android:pathData="M7,4
|
||||
l34,0
|
||||
@@ -23,7 +23,7 @@
|
||||
/>
|
||||
|
||||
<!-- Block cursor. -->
|
||||
<path android:fillColor="#000"
|
||||
<path android:fillColor="#FFF"
|
||||
android:pathData="M14,14
|
||||
l5,0
|
||||
l0,10
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
android:orientation="vertical"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<android.support.v4.widget.DrawerLayout
|
||||
android:id="@+id/drawer_layout"
|
||||
@@ -69,7 +70,7 @@
|
||||
android:id="@+id/viewpager"
|
||||
android:visibility="gone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:layout_height="75dp"
|
||||
android:background="@android:drawable/screen_background_dark_transparent"
|
||||
android:layout_alignParentBottom="true" />
|
||||
</RelativeLayout>
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
<string name="toggle_soft_keyboard">Keyboard</string>
|
||||
<string name="reset_terminal">Reset</string>
|
||||
<string name="style_terminal">Style</string>
|
||||
<string name="toggle_fullscreen">Fullscreen</string>
|
||||
<string name="share_transcript_title">Terminal transcript</string>
|
||||
<string name="help">Help</string>
|
||||
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
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>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:2.3.3'
|
||||
classpath 'com.android.tools.build:gradle:3.1.3'
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
}
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
3
gradle/wrapper/gradle-wrapper.properties
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,5 @@
|
||||
#Sun Jun 11 22:47:00 CEST 2017
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.7-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-bin.zip
|
||||
|
||||
6
gradlew
vendored
6
gradlew
vendored
@@ -33,11 +33,11 @@ DEFAULT_JVM_OPTS=""
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn ( ) {
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die ( ) {
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
@@ -155,7 +155,7 @@ if $cygwin ; then
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save ( ) {
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// Start https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle
|
||||
apply plugin: 'com.github.dcendents.android-maven'
|
||||
group = publishedGroupId // Maven Group ID for the artifact
|
||||
install {
|
||||
repositories.mavenInstaller {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
plugins {
|
||||
id "com.jfrog.bintray" version "1.7"
|
||||
id "com.github.dcendents.android-maven" version "1.5"
|
||||
id "com.jfrog.bintray" version "1.7.3"
|
||||
id "com.github.dcendents.android-maven" version "2.0"
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
@@ -13,16 +13,15 @@ ext {
|
||||
libraryDescription = 'The terminal emulator used in Termux'
|
||||
siteUrl = 'https://github.com/termux/termux'
|
||||
gitUrl = 'https://github.com/termux/termux.git'
|
||||
libraryVersion = '0.50'
|
||||
libraryVersion = '0.52'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 25
|
||||
buildToolsVersion "25.0.3"
|
||||
compileSdkVersion 27
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 25
|
||||
targetSdkVersion 27
|
||||
|
||||
externalNativeBuild {
|
||||
ndkBuild {
|
||||
@@ -56,7 +55,7 @@ tasks.withType(Test) {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testCompile 'junit:junit:4.12'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
}
|
||||
|
||||
apply from: '../scripts/bintray-publish.gradle'
|
||||
|
||||
@@ -223,6 +223,7 @@ public final class TerminalEmulator {
|
||||
|
||||
private byte mUtf8ToFollow, mUtf8Index;
|
||||
private final byte[] mUtf8InputBuffer = new byte[4];
|
||||
private int mLastEmittedCodePoint = -1;
|
||||
|
||||
public final TerminalColors mColors = new TerminalColors();
|
||||
|
||||
@@ -300,6 +301,11 @@ public final class TerminalEmulator {
|
||||
* @param mouseButton one of the MOUSE_* constants of this class.
|
||||
*/
|
||||
public void sendMouseEvent(int mouseButton, int column, int row, boolean pressed) {
|
||||
if (column < 1) column = 1;
|
||||
if (column > mColumns) column = mColumns;
|
||||
if (row < 1) row = 1;
|
||||
if (row > mRows) row = mRows;
|
||||
|
||||
if (mouseButton == MOUSE_LEFT_BUTTON_MOVED && !isDecsetInternalBitSet(DECSET_BIT_MOUSE_TRACKING_BUTTON_EVENT)) {
|
||||
// Do not send tracking.
|
||||
} else if (isDecsetInternalBitSet(DECSET_BIT_MOUSE_PROTOCOL_SGR)) {
|
||||
@@ -307,7 +313,7 @@ public final class TerminalEmulator {
|
||||
} else {
|
||||
mouseButton = pressed ? mouseButton : 3; // 3 for release of all buttons.
|
||||
// Clip to screen, and clip to the limits of 8-bit data.
|
||||
boolean out_of_bounds = column < 1 || row < 1 || column > mColumns || row > mRows || column > 255 - 32 || row > 255 - 32;
|
||||
boolean out_of_bounds = column > 255 - 32 || row > 255 - 32;
|
||||
if (!out_of_bounds) {
|
||||
byte[] data = {'\033', '[', 'M', (byte) (32 + mouseButton), (byte) (32 + column), (byte) (32 + row)};
|
||||
mSession.write(data, 0, data.length);
|
||||
@@ -419,10 +425,11 @@ public final class TerminalEmulator {
|
||||
mUtf8Index = mUtf8ToFollow = 0;
|
||||
|
||||
if (codePoint >= 0x80 && codePoint <= 0x9F) {
|
||||
// Sequence decoded to a C1 control character which is the same as escape followed by
|
||||
// ((code & 0x7F) + 0x40).
|
||||
processCodePoint(/* escape (hexadecimal=0x1B, octal=033): */27);
|
||||
processCodePoint((codePoint & 0x7F) + 0x40);
|
||||
// Sequence decoded to a C1 control character which we ignore. They are
|
||||
// not used nowadays and increases the risk of messing up the terminal state
|
||||
// on binary input. XTerm does not allow them in utf-8:
|
||||
// "It is not possible to use a C1 control obtained from decoding the
|
||||
// UTF-8 text" - http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
||||
} else {
|
||||
switch (Character.getType(codePoint)) {
|
||||
case Character.UNASSIGNED:
|
||||
@@ -1190,12 +1197,20 @@ public final class TerminalEmulator {
|
||||
}
|
||||
|
||||
private void doLinefeed() {
|
||||
boolean belowScrollingRegion = mCursorRow >= mBottomMargin;
|
||||
int newCursorRow = mCursorRow + 1;
|
||||
if (newCursorRow >= mBottomMargin) {
|
||||
scrollDownOneLine();
|
||||
newCursorRow = mBottomMargin - 1;
|
||||
if (belowScrollingRegion) {
|
||||
// Move down (but not scroll) as long as we are above the last row.
|
||||
if (mCursorRow != mRows - 1) {
|
||||
setCursorRow(newCursorRow);
|
||||
}
|
||||
} else {
|
||||
if (newCursorRow == mBottomMargin) {
|
||||
scrollDownOneLine();
|
||||
newCursorRow = mBottomMargin - 1;
|
||||
}
|
||||
setCursorRow(newCursorRow);
|
||||
}
|
||||
setCursorRow(newCursorRow);
|
||||
}
|
||||
|
||||
private void continueSequence(int state) {
|
||||
@@ -1508,6 +1523,11 @@ public final class TerminalEmulator {
|
||||
case '`': // Horizontal position absolute (HPA - http://www.vt100.net/docs/vt510-rm/HPA).
|
||||
setCursorColRespectingOriginMode(getArg0(1) - 1);
|
||||
break;
|
||||
case 'b': // Repeat the preceding graphic character Ps times (REP).
|
||||
if (mLastEmittedCodePoint == -1) break;
|
||||
final int numRepeat = getArg0(1);
|
||||
for (int i = 0; i < numRepeat; i++) emitCodePoint(mLastEmittedCodePoint);
|
||||
break;
|
||||
case 'c': // Primary Device Attributes (http://www.vt100.net/docs/vt510-rm/DA1) if argument is missing or zero.
|
||||
// The important part that may still be used by some (tmux stores this value but does not currently use it)
|
||||
// is the first response parameter identifying the terminal service class, where we send 64 for "vt420".
|
||||
@@ -1564,7 +1584,7 @@ public final class TerminalEmulator {
|
||||
break;
|
||||
case 'r': // "CSI${top};${bottom}r" - set top and bottom Margins (DECSTBM).
|
||||
{
|
||||
// http://www.vt100.net/docs/vt510-rm/DECSTBM
|
||||
// https://vt100.net/docs/vt510-rm/DECSTBM.html
|
||||
// The top margin defaults to 1, the bottom margin defaults to mRows.
|
||||
// The escape sequence numbers top 1..23, but we number top 0..22.
|
||||
// The escape sequence numbers bottom 2..24, and so do we (because we use a zero based numbering
|
||||
@@ -1573,6 +1593,7 @@ public final class TerminalEmulator {
|
||||
// Also require that top + 2 <= bottom.
|
||||
mTopMargin = Math.max(0, Math.min(getArg0(1) - 1, mRows - 2));
|
||||
mBottomMargin = Math.max(mTopMargin + 2, Math.min(getArg1(mRows), mRows));
|
||||
|
||||
// DECSTBM moves the cursor to column 1, line 1 of the page respecting origin mode.
|
||||
setCursorPosition(0, 0);
|
||||
}
|
||||
@@ -2090,6 +2111,7 @@ public final class TerminalEmulator {
|
||||
* @param codePoint The code point of the character to display
|
||||
*/
|
||||
private void emitCodePoint(int codePoint) {
|
||||
mLastEmittedCodePoint = codePoint;
|
||||
if (mUseLineDrawingUsesG0 ? mUseLineDrawingG0 : mUseLineDrawingG1) {
|
||||
// http://www.vt100.net/docs/vt102-ug/table5-15.html.
|
||||
switch (codePoint) {
|
||||
@@ -2316,6 +2338,9 @@ public final class TerminalEmulator {
|
||||
public void paste(String text) {
|
||||
// First: Always remove escape key and C1 control characters [0x80,0x9F]:
|
||||
text = text.replaceAll("(\u001B|[\u0080-\u009F])", "");
|
||||
// Second: Replace all newlines (\n) or CRLF (\r\n) with carriage returns (\r).
|
||||
text = text.replaceAll("\r?\n", "\r");
|
||||
|
||||
// Then: Implement bracketed paste mode if enabled:
|
||||
boolean bracketed = isDecsetInternalBitSet(DECSET_BIT_BRACKETED_PASTE_MODE);
|
||||
if (bracketed) mSession.write("\033[200~");
|
||||
|
||||
@@ -21,6 +21,8 @@ public final class TerminalRow {
|
||||
boolean mLineWrap;
|
||||
/** The style bits of each cell in the row. See {@link TextStyle}. */
|
||||
final long[] mStyle;
|
||||
/** If this row might contain chars with width != 1, used for deactivating fast path */
|
||||
boolean mHasNonOneWidthOrSurrogateChars;
|
||||
|
||||
/** Construct a blank row (containing only whitespace, ' ') with a specified style. */
|
||||
public TerminalRow(int columns, long style) {
|
||||
@@ -32,6 +34,7 @@ public final class TerminalRow {
|
||||
|
||||
/** NOTE: The sourceX2 is exclusive. */
|
||||
public void copyInterval(TerminalRow line, int sourceX1, int sourceX2, int destinationX) {
|
||||
mHasNonOneWidthOrSurrogateChars |= line.mHasNonOneWidthOrSurrogateChars;
|
||||
final int x1 = line.findStartOfColumn(sourceX1);
|
||||
final int x2 = line.findStartOfColumn(sourceX2);
|
||||
boolean startingFromSecondHalfOfWideChar = (sourceX1 > 0 && line.wideDisplayCharacterStartingAt(sourceX1 - 1));
|
||||
@@ -116,6 +119,7 @@ public final class TerminalRow {
|
||||
Arrays.fill(mText, ' ');
|
||||
Arrays.fill(mStyle, style);
|
||||
mSpaceUsed = (short) mColumns;
|
||||
mHasNonOneWidthOrSurrogateChars = false;
|
||||
}
|
||||
|
||||
// https://github.com/steven676/Android-Terminal-Emulator/commit/9a47042620bec87617f0b4f5d50568535668fe26
|
||||
@@ -123,6 +127,17 @@ public final class TerminalRow {
|
||||
mStyle[columnToSet] = style;
|
||||
|
||||
final int newCodePointDisplayWidth = WcWidth.width(codePoint);
|
||||
|
||||
// Fast path when we don't have any chars with width != 1
|
||||
if (!mHasNonOneWidthOrSurrogateChars) {
|
||||
if (codePoint >= Character.MIN_SUPPLEMENTARY_CODE_POINT || newCodePointDisplayWidth != 1) {
|
||||
mHasNonOneWidthOrSurrogateChars = true;
|
||||
} else {
|
||||
mText[columnToSet] = (char) codePoint;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final boolean newIsCombining = newCodePointDisplayWidth <= 0;
|
||||
|
||||
boolean wasExtraColForWideChar = (columnToSet > 0) && wideDisplayCharacterStartingAt(columnToSet - 1);
|
||||
|
||||
@@ -35,4 +35,15 @@ public class ControlSequenceIntroducerTest extends TerminalTestCase {
|
||||
withTerminalSized(3, 2).enterString("\033[0;38;2;255;255;255;48;2;0;0;0;1;2;3;4;5;7;8;9mabc").assertLinesAre("abc", " ");
|
||||
}
|
||||
|
||||
/** CSI Ps b Repeat the preceding graphic character Ps times (REP). */
|
||||
public void testRepeat() {
|
||||
withTerminalSized(3, 2).enterString("a\033[b").assertLinesAre("aa ", " ");
|
||||
withTerminalSized(3, 2).enterString("a\033[2b").assertLinesAre("aaa", " ");
|
||||
// When no char has been output we ignore REP:
|
||||
withTerminalSized(3, 2).enterString("\033[b").assertLinesAre(" ", " ");
|
||||
// This shows that REP outputs the last emitted code point and not the one relative to the
|
||||
// current cursor position:
|
||||
withTerminalSized(5, 2).enterString("abcde\033[2G\033[2b\n").assertLinesAre("aeede", " ");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -98,4 +98,13 @@ public class ScrollRegionTest extends TerminalTestCase {
|
||||
withTerminalSized(2, 5).enterString("1\r\n2\r\n3\r\n4\r\n5").assertLinesAre("1 ", "2 ", "3 ", "4 ", "5 ");
|
||||
enterString("\033[3r").enterString("\033[2T").assertLinesAre("1 ", "2 ", " ", " ", "3 ");
|
||||
}
|
||||
|
||||
public void testScrollDownBelowScrollRegion() {
|
||||
withTerminalSized(2, 5).enterString("1\r\n2\r\n3\r\n4\r\n5").assertLinesAre("1 ", "2 ", "3 ", "4 ", "5 ");
|
||||
enterString("\033[1;3r"); // DECSTBM margins.
|
||||
enterString("\033[4;1H"); // Place cursor just below bottom margin.
|
||||
enterString("QQ\r\nRR\r\n\r\n\r\nYY");
|
||||
assertLinesAre("1 ", "2 ", "3 ", "QQ", "YY");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ public class TerminalRowTest extends TestCase {
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
row = new TerminalRow(COLUMNS, TextStyle.NORMAL);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,14 @@ public class TerminalTest extends TerminalTestCase {
|
||||
assertEquals("\033[<0;3;4M", mOutput.getOutputAndClear());
|
||||
mTerminal.sendMouseEvent(TerminalEmulator.MOUSE_LEFT_BUTTON, 3, 4, false);
|
||||
assertEquals("\033[<0;3;4m", mOutput.getOutputAndClear());
|
||||
|
||||
// When the client says that a click is outside (which could happen when pixels are outside
|
||||
// the terminal area, see https://github.com/termux/termux-app/issues/501) the terminal
|
||||
// sends a click at the edge.
|
||||
mTerminal.sendMouseEvent(TerminalEmulator.MOUSE_LEFT_BUTTON, 0, 0, true);
|
||||
assertEquals("\033[<0;1;1M", mOutput.getOutputAndClear());
|
||||
mTerminal.sendMouseEvent(TerminalEmulator.MOUSE_LEFT_BUTTON, 11, 11, false);
|
||||
assertEquals("\033[<0;10;10m", mOutput.getOutputAndClear());
|
||||
}
|
||||
|
||||
public void testNormalization() throws UnsupportedEncodingException {
|
||||
|
||||
@@ -103,6 +103,7 @@ public abstract class TerminalTestCase extends TestCase {
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
mOutput = new MockTerminalOutput();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
plugins {
|
||||
id "com.jfrog.bintray" version "1.7"
|
||||
id "com.github.dcendents.android-maven" version "1.5"
|
||||
id "com.jfrog.bintray" version "1.7.3"
|
||||
id "com.github.dcendents.android-maven" version "2.0"
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
@@ -13,21 +13,20 @@ ext {
|
||||
libraryDescription = 'The terminal view used in Termux'
|
||||
siteUrl = 'https://github.com/termux/termux'
|
||||
gitUrl = 'https://github.com/termux/termux.git'
|
||||
libraryVersion = '0.49'
|
||||
libraryVersion = '0.50'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 25
|
||||
buildToolsVersion "25.0.3"
|
||||
compileSdkVersion 27
|
||||
|
||||
dependencies {
|
||||
compile 'com.android.support:support-annotations:25.3.1'
|
||||
compile project(":terminal-emulator")
|
||||
implementation 'com.android.support:support-annotations:27.1.1'
|
||||
api project(":terminal-emulator")
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 25
|
||||
targetSdkVersion 27
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -40,7 +39,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testCompile 'junit:junit:4.12'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
}
|
||||
|
||||
apply from: '../scripts/bintray-publish.gradle'
|
||||
|
||||
@@ -85,6 +85,7 @@ final class GestureAndScaleRecognizer {
|
||||
return mListener.onScale(detector.getFocusX(), detector.getFocusY(), detector.getScaleFactor());
|
||||
}
|
||||
});
|
||||
mScaleDetector.setQuickScaleEnabled(false);
|
||||
}
|
||||
|
||||
public void onTouchEvent(MotionEvent event) {
|
||||
|
||||
@@ -16,7 +16,7 @@ import com.termux.terminal.WcWidth;
|
||||
* <p/>
|
||||
* Saves font metrics, so needs to be recreated each time the typeface or font size changes.
|
||||
*/
|
||||
final class TerminalRenderer {
|
||||
public final class TerminalRenderer {
|
||||
|
||||
final int mTextSize;
|
||||
final Typeface mTypeface;
|
||||
|
||||
@@ -642,6 +642,8 @@ public final class TerminalView extends View {
|
||||
+ leftAltDownFromEvent + ")");
|
||||
}
|
||||
|
||||
if (mTermSession == null) return;
|
||||
|
||||
final boolean controlDown = controlDownFromEvent || mClient.readControlKey();
|
||||
final boolean altDown = leftAltDownFromEvent || mClient.readAltKey();
|
||||
|
||||
@@ -834,6 +836,10 @@ public final class TerminalView extends View {
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
if (!mIsSelectingText) {
|
||||
// Fix issue where the dialog is pressed while being dismissed.
|
||||
return true;
|
||||
}
|
||||
switch (item.getItemId()) {
|
||||
case 1:
|
||||
String selectedText = mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2).trim();
|
||||
|
||||
Reference in New Issue
Block a user