Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3aaa0ab267 | ||
|
|
e7f9647beb | ||
|
|
5558f371b4 | ||
|
|
0882ed6470 | ||
|
|
5c02448521 | ||
|
|
17382fb190 | ||
|
|
d6eea83bfc | ||
|
|
51181c2d49 | ||
|
|
480b8a4f7e | ||
|
|
f989157f10 | ||
|
|
0e942f90a6 | ||
|
|
5b8eca46a1 | ||
|
|
493900d60b | ||
|
|
c6d6a63637 | ||
|
|
ca71265f23 | ||
|
|
46c9c4b80e | ||
|
|
6ca055bb25 |
@@ -12,3 +12,5 @@ root = true
|
|||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ android {
|
|||||||
applicationId "com.termux"
|
applicationId "com.termux"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 23
|
targetSdkVersion 23
|
||||||
versionCode 29
|
versionCode 31
|
||||||
versionName "0.29"
|
versionName "0.31"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@@ -43,6 +43,21 @@
|
|||||||
android:parentActivityName=".app.TermuxActivity"
|
android:parentActivityName=".app.TermuxActivity"
|
||||||
android:label="@string/application_name" />
|
android:label="@string/application_name" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name="com.termux.filepicker.TermuxFileReceiverActivity"
|
||||||
|
android:label="@string/application_name"
|
||||||
|
android:taskAffinity="com.termux.filereceiver"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:noHistory="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<action android:name="android.intent.action.EDIT" />
|
||||||
|
<action android:name="android.intent.action.SEND"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="*/*" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.termux.filepicker.TermuxFilePickerActivity"
|
android:name="com.termux.filepicker.TermuxFilePickerActivity"
|
||||||
android:label="@string/application_name"
|
android:label="@string/application_name"
|
||||||
|
|||||||
@@ -3,24 +3,25 @@ package com.termux.app;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.text.Selection;
|
import android.text.Selection;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.ViewGroup.LayoutParams;
|
import android.view.ViewGroup.LayoutParams;
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
final class DialogUtils {
|
public final class DialogUtils {
|
||||||
|
|
||||||
public interface TextSetListener {
|
public interface TextSetListener {
|
||||||
void onTextSet(String text);
|
void onTextSet(String text);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void textInput(Activity activity, int titleText, int positiveButtonText, String initialText, final TextSetListener onPositive,
|
public static void textInput(Activity activity, int titleText, String initialText,
|
||||||
int neutralButtonText, final TextSetListener onNeutral) {
|
int positiveButtonText, final TextSetListener onPositive,
|
||||||
|
int neutralButtonText, final TextSetListener onNeutral,
|
||||||
|
int negativeButtonText, final TextSetListener onNegative,
|
||||||
|
final DialogInterface.OnDismissListener onDismiss) {
|
||||||
final EditText input = new EditText(activity);
|
final EditText input = new EditText(activity);
|
||||||
input.setSingleLine();
|
input.setSingleLine();
|
||||||
if (initialText != null) {
|
if (initialText != null) {
|
||||||
@@ -57,23 +58,32 @@ final class DialogUtils {
|
|||||||
public void onClick(DialogInterface d, int whichButton) {
|
public void onClick(DialogInterface d, int whichButton) {
|
||||||
onPositive.onTextSet(input.getText().toString());
|
onPositive.onTextSet(input.getText().toString());
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.setNegativeButton(android.R.string.cancel, null);
|
|
||||||
|
|
||||||
if (onNeutral != null) {
|
|
||||||
builder.setNeutralButton(neutralButtonText, new DialogInterface.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
onNeutral.onTextSet(input.getText().toString());
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
if (onNeutral != null) {
|
||||||
|
builder.setNeutralButton(neutralButtonText, new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
onNeutral.onTextSet(input.getText().toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onNegative == null) {
|
||||||
|
builder.setNegativeButton(android.R.string.cancel, null);
|
||||||
|
} else {
|
||||||
|
builder.setNegativeButton(negativeButtonText, new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
onNegative.onTextSet(input.getText().toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onDismiss != null) builder.setOnDismissListener(onDismiss);
|
||||||
|
|
||||||
dialogHolder[0] = builder.create();
|
dialogHolder[0] = builder.create();
|
||||||
if ((activity.getResources().getConfiguration().hardKeyboardHidden & Configuration.HARDKEYBOARDHIDDEN_YES) == 0) {
|
dialogHolder[0].setCanceledOnTouchOutside(false);
|
||||||
// Show soft keyboard unless hardware keyboard available.
|
|
||||||
dialogHolder[0].getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
|
||||||
}
|
|
||||||
dialogHolder[0].show();
|
dialogHolder[0].show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -287,28 +287,27 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
newSessionButton.setOnLongClickListener(new OnLongClickListener() {
|
newSessionButton.setOnLongClickListener(new OnLongClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public boolean onLongClick(View v) {
|
public boolean onLongClick(View v) {
|
||||||
Resources res = getResources();
|
DialogUtils.textInput(TermuxActivity.this, R.string.session_new_named_title, null, R.string.session_new_named_positive_button,
|
||||||
DialogUtils.textInput(TermuxActivity.this, R.string.session_new_named_title, R.string.session_new_named_positive_button, null,
|
new DialogUtils.TextSetListener() {
|
||||||
new DialogUtils.TextSetListener() {
|
@Override
|
||||||
@Override
|
public void onTextSet(String text) {
|
||||||
public void onTextSet(String text) {
|
addNewSession(false, text);
|
||||||
addNewSession(false, text);
|
}
|
||||||
}
|
}, R.string.new_session_failsafe, new DialogUtils.TextSetListener() {
|
||||||
}, R.string.new_session_failsafe, new DialogUtils.TextSetListener() {
|
@Override
|
||||||
@Override
|
public void onTextSet(String text) {
|
||||||
public void onTextSet(String text) {
|
addNewSession(true, text);
|
||||||
addNewSession(true, text);
|
}
|
||||||
}
|
}
|
||||||
}
|
, -1, null, null);
|
||||||
);
|
return true;
|
||||||
return true;
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
findViewById(R.id.toggle_keyboard_button).setOnClickListener(new OnClickListener() {
|
findViewById(R.id.toggle_keyboard_button).setOnClickListener(new OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
@@ -492,14 +491,13 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
|
|
||||||
@SuppressLint("InflateParams")
|
@SuppressLint("InflateParams")
|
||||||
void renameSession(final TerminalSession sessionToRename) {
|
void renameSession(final TerminalSession sessionToRename) {
|
||||||
DialogUtils.textInput(this, R.string.session_rename_title, R.string.session_rename_positive_button, sessionToRename.mSessionName,
|
DialogUtils.textInput(this, R.string.session_rename_title, sessionToRename.mSessionName, R.string.session_rename_positive_button, new DialogUtils.TextSetListener() {
|
||||||
new DialogUtils.TextSetListener() {
|
@Override
|
||||||
@Override
|
public void onTextSet(String text) {
|
||||||
public void onTextSet(String text) {
|
sessionToRename.mSessionName = text;
|
||||||
sessionToRename.mSessionName = text;
|
}
|
||||||
}
|
}, -1, null, -1, null, null);
|
||||||
}, -1, null);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onServiceDisconnected(ComponentName name) {
|
public void onServiceDisconnected(ComponentName name) {
|
||||||
@@ -731,21 +729,23 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
case CONTEXTMENU_STYLING_ID: {
|
case CONTEXTMENU_STYLING_ID: {
|
||||||
Intent stylingIntent = new Intent();
|
Intent stylingIntent = new Intent();
|
||||||
stylingIntent.setClassName("com.termux.styling", "com.termux.styling.TermuxStyleActivity");
|
stylingIntent.setClassName("com.termux.styling", "com.termux.styling.TermuxStyleActivity");
|
||||||
try {
|
try {
|
||||||
startActivity(stylingIntent);
|
startActivity(stylingIntent);
|
||||||
} catch (ActivityNotFoundException e) {
|
} catch (ActivityNotFoundException | IllegalArgumentException e) {
|
||||||
new AlertDialog.Builder(this).setMessage(R.string.styling_not_installed)
|
// The startActivity() call is not documented to throw IllegalArgumentException.
|
||||||
.setPositiveButton(R.string.styling_install, new android.content.DialogInterface.OnClickListener() {
|
// However, crash reporting shows that it sometimes does, so catch it here.
|
||||||
@Override
|
new AlertDialog.Builder(this).setMessage(R.string.styling_not_installed)
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
.setPositiveButton(R.string.styling_install, new android.content.DialogInterface.OnClickListener() {
|
||||||
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://play.google.com/store/apps/details?id=com.termux.styling")));
|
@Override
|
||||||
}
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
}).setNegativeButton(android.R.string.cancel, null).show();
|
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://play.google.com/store/apps/details?id=com.termux.styling")));
|
||||||
}
|
}
|
||||||
}
|
}).setNegativeButton(android.R.string.cancel, null).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
case CONTEXTMENU_TOGGLE_FULLSCREEN_ID:
|
case CONTEXTMENU_TOGGLE_FULLSCREEN_ID:
|
||||||
toggleImmersive();
|
toggleImmersive();
|
||||||
|
|||||||
@@ -176,17 +176,19 @@ final class TermuxInstaller {
|
|||||||
}.start();
|
}.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get bootstrap zip url for this systems cpu architecture. */
|
/** Get bootstrap zip url for this systems cpu architecture. */
|
||||||
static URL determineZipUrl() throws MalformedURLException {
|
static URL determineZipUrl() throws MalformedURLException {
|
||||||
String arch = System.getProperty("os.arch");
|
String arch = System.getProperty("os.arch");
|
||||||
if (arch.startsWith("arm") || arch.equals("aarch64")) {
|
if (arch.startsWith("armv8")) {
|
||||||
// Handle different arm variants such as armv7l:
|
arch = "aarch64";
|
||||||
arch = "arm";
|
} else if (arch.startsWith("arm")) {
|
||||||
} else if (arch.startsWith("x86")) { // "x86" on arcwelder, "x86_64" on 64-bit android.
|
// Handle different arm variants such as armv7l:
|
||||||
arch = "i686";
|
arch = "arm";
|
||||||
}
|
} else if (arch.startsWith("x86")) { // "x86" on arcwelder, "x86_64" on 64-bit android.
|
||||||
return new URL("https://termux.net/bootstrap/bootstrap-" + arch + ".zip");
|
arch = "i686";
|
||||||
}
|
}
|
||||||
|
return new URL("https://termux.net/bootstrap/bootstrap-" + arch + ".zip");
|
||||||
|
}
|
||||||
|
|
||||||
/** Delete a folder and all its content or throw. */
|
/** Delete a folder and all its content or throw. */
|
||||||
static void deleteFolder(File fileOrDirectory) {
|
static void deleteFolder(File fileOrDirectory) {
|
||||||
|
|||||||
@@ -55,7 +55,11 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
|||||||
/** Intent action to toggle the wifi lock, {@link #mWifiLock}, which this service may hold. */
|
/** Intent action to toggle the wifi lock, {@link #mWifiLock}, which this service may hold. */
|
||||||
private static final String ACTION_LOCK_WIFI = "com.termux.service_toggle_wifi_lock";
|
private static final String ACTION_LOCK_WIFI = "com.termux.service_toggle_wifi_lock";
|
||||||
/** Intent action to launch a new terminal session. Executed from TermuxWidgetProvider. */
|
/** Intent action to launch a new terminal session. Executed from TermuxWidgetProvider. */
|
||||||
private static final String ACTION_EXECUTE = "com.termux.service_execute";
|
public static final String ACTION_EXECUTE = "com.termux.service_execute";
|
||||||
|
|
||||||
|
public static final String EXTRA_ARGUMENTS = "com.termux.execute.arguments";
|
||||||
|
|
||||||
|
public static final String EXTRA_CURRENT_WORKING_DIRECTORY = "com.termux.execute.cwd";
|
||||||
|
|
||||||
/** This service is only bound from inside the same process and never uses IPC. */
|
/** This service is only bound from inside the same process and never uses IPC. */
|
||||||
class LocalBinder extends Binder {
|
class LocalBinder extends Binder {
|
||||||
@@ -113,8 +117,8 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
|||||||
} else if (ACTION_EXECUTE.equals(action)) {
|
} else if (ACTION_EXECUTE.equals(action)) {
|
||||||
Uri executableUri = intent.getData();
|
Uri executableUri = intent.getData();
|
||||||
String executablePath = (executableUri == null ? null : executableUri.getPath());
|
String executablePath = (executableUri == null ? null : executableUri.getPath());
|
||||||
String[] arguments = (executableUri == null ? null : intent.getStringArrayExtra("com.termux.execute.arguments"));
|
String[] arguments = (executableUri == null ? null : intent.getStringArrayExtra(EXTRA_ARGUMENTS));
|
||||||
String cwd = intent.getStringExtra("com.termux.execute.cwd");
|
String cwd = intent.getStringExtra(EXTRA_CURRENT_WORKING_DIRECTORY);
|
||||||
TerminalSession newSession = createTermSession(executablePath, arguments, cwd, false);
|
TerminalSession newSession = createTermSession(executablePath, arguments, cwd, false);
|
||||||
|
|
||||||
// Transform executable path to session name, e.g. "/bin/do-something.sh" => "do something.sh".
|
// Transform executable path to session name, e.g. "/bin/do-something.sh" => "do something.sh".
|
||||||
@@ -237,17 +241,22 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
|||||||
final String prefixEnv = "PREFIX=" + PREFIX_PATH;
|
final String prefixEnv = "PREFIX=" + PREFIX_PATH;
|
||||||
final String androidRootEnv = "ANDROID_ROOT=" + System.getenv("ANDROID_ROOT");
|
final String androidRootEnv = "ANDROID_ROOT=" + System.getenv("ANDROID_ROOT");
|
||||||
final String androidDataEnv = "ANDROID_DATA=" + System.getenv("ANDROID_DATA");
|
final String androidDataEnv = "ANDROID_DATA=" + System.getenv("ANDROID_DATA");
|
||||||
|
// EXTERNAL_STORAGE is needed for /system/bin/am to work on at least
|
||||||
|
// Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3.
|
||||||
|
final String externalStorageEnv = "EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE");
|
||||||
String[] env;
|
String[] env;
|
||||||
if (failSafe) {
|
if (failSafe) {
|
||||||
env = new String[] { termEnv, homeEnv, prefixEnv, androidRootEnv, androidDataEnv };
|
// Keep the default path so that system binaries can be used in the failsafe session.
|
||||||
|
final String pathEnv = "PATH=" + System.getenv("PATH");
|
||||||
|
env = new String[] { termEnv, homeEnv, prefixEnv, androidRootEnv, androidDataEnv, pathEnv, externalStorageEnv };
|
||||||
} else {
|
} else {
|
||||||
final String ps1Env = "PS1=$ ";
|
final String ps1Env = "PS1=$ ";
|
||||||
final String ldEnv = "LD_LIBRARY_PATH=" + PREFIX_PATH + "/lib";
|
final String ldEnv = "LD_LIBRARY_PATH=" + PREFIX_PATH + "/lib";
|
||||||
final String langEnv = "LANG=en_US.UTF-8";
|
final String langEnv = "LANG=en_US.UTF-8";
|
||||||
final String pathEnv = "PATH=" + PREFIX_PATH + "/bin:" + PREFIX_PATH + "/bin/applets:" + System.getenv("PATH");
|
final String pathEnv = "PATH=" + PREFIX_PATH + "/bin:" + PREFIX_PATH + "/bin/applets";
|
||||||
final String pwdEnv = "PWD=" + cwd;
|
final String pwdEnv = "PWD=" + cwd;
|
||||||
|
|
||||||
env = new String[] { termEnv, homeEnv, prefixEnv, ps1Env, ldEnv, langEnv, pathEnv, pwdEnv, androidRootEnv, androidDataEnv };
|
env = new String[] { termEnv, homeEnv, prefixEnv, ps1Env, ldEnv, langEnv, pathEnv, pwdEnv, androidRootEnv, androidDataEnv, externalStorageEnv };
|
||||||
}
|
}
|
||||||
|
|
||||||
String shellName;
|
String shellName;
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
package com.termux.filepicker;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.provider.OpenableColumns;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.util.Patterns;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.app.DialogUtils;
|
||||||
|
import com.termux.app.TermuxService;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
public class TermuxFileReceiverActivity extends Activity {
|
||||||
|
|
||||||
|
static final String TERMUX_RECEIVEDIR = TermuxService.FILES_PATH + "/home/downloads";
|
||||||
|
static final String EDITOR_PROGRAM = TermuxService.HOME_PATH + "/bin/termux-file-editor";
|
||||||
|
static final String URL_OPENER_PROGRAM = TermuxService.HOME_PATH + "/bin/termux-url-opener";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the activity should be finished when the name input dialog is dismissed. This is disabled
|
||||||
|
* before showing an error dialog, since the act of showing the error dialog will cause the
|
||||||
|
* name input dialog to be implicitly dismissed, and we do not want to finish the activity directly
|
||||||
|
* when showing the error dialog.
|
||||||
|
*/
|
||||||
|
private boolean mFinishOnDismissNameDialog = true;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
|
||||||
|
final Intent intent = getIntent();
|
||||||
|
final String action = intent.getAction();
|
||||||
|
final String type = intent.getType();
|
||||||
|
final String scheme = intent.getScheme();
|
||||||
|
|
||||||
|
if (intent.getExtras() == null) {
|
||||||
|
Log.e("termux", "NULL EXTRAS");
|
||||||
|
} else {
|
||||||
|
for (String key : intent.getExtras().keySet()) {
|
||||||
|
Object value = intent.getExtras().get(key);
|
||||||
|
Log.d("termux", String.format("Extra %s %s (%s)", key,
|
||||||
|
value.toString(), value.getClass().getName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Intent.ACTION_SEND.equals(action) && type != null) {
|
||||||
|
final String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||||
|
final Uri sharedUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||||
|
|
||||||
|
if (sharedText != null) {
|
||||||
|
if (Patterns.WEB_URL.matcher(sharedText).matches()) {
|
||||||
|
handleUrlAndFinish(sharedText);
|
||||||
|
} else {
|
||||||
|
String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
|
||||||
|
if (subject == null) subject = intent.getStringExtra(Intent.EXTRA_TITLE);
|
||||||
|
if (subject != null) subject += ".txt";
|
||||||
|
promptNameAndSave(new ByteArrayInputStream(sharedText.getBytes(StandardCharsets.UTF_8)), subject);
|
||||||
|
}
|
||||||
|
} else if (sharedUri != null) {
|
||||||
|
handleContentUri(sharedUri, intent.getStringExtra(Intent.EXTRA_TITLE));
|
||||||
|
} else {
|
||||||
|
showErrorDialogAndQuit("Send action without content - nothing to save.");
|
||||||
|
}
|
||||||
|
} else if (scheme.equals("content")) {
|
||||||
|
handleContentUri(intent.getData(), intent.getStringExtra(Intent.EXTRA_TITLE));
|
||||||
|
} else if (scheme.equals("file")) {
|
||||||
|
// When e.g. clicking on a downloaded apk:
|
||||||
|
String path = intent.getData().getPath();
|
||||||
|
File file = new File(path);
|
||||||
|
try {
|
||||||
|
FileInputStream in = new FileInputStream(file);
|
||||||
|
promptNameAndSave(in, file.getName());
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
showErrorDialogAndQuit("Cannot open file: " + e.getMessage() + ".");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showErrorDialogAndQuit("Unhandled scheme: " + intent.getScheme() + ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void showErrorDialogAndQuit(String message) {
|
||||||
|
mFinishOnDismissNameDialog = false;
|
||||||
|
new AlertDialog.Builder(this).setMessage(message).setOnDismissListener(new DialogInterface.OnDismissListener() {
|
||||||
|
@Override
|
||||||
|
public void onDismiss(DialogInterface dialog) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}).setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleContentUri(final Uri uri, String subjectFromIntent) {
|
||||||
|
try {
|
||||||
|
String attachmentFileName = null;
|
||||||
|
|
||||||
|
String[] projection = new String[]{OpenableColumns.DISPLAY_NAME};
|
||||||
|
try (Cursor c = getContentResolver().query(uri, projection, null, null, null)) {
|
||||||
|
if (c != null && c.moveToFirst()) {
|
||||||
|
final int fileNameColumnId = c.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||||
|
if (fileNameColumnId >= 0) attachmentFileName = c.getString(fileNameColumnId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attachmentFileName == null) attachmentFileName = subjectFromIntent;
|
||||||
|
|
||||||
|
InputStream in = getContentResolver().openInputStream(uri);
|
||||||
|
promptNameAndSave(in, attachmentFileName);
|
||||||
|
} catch (Exception e) {
|
||||||
|
showErrorDialogAndQuit("Unable to handle shared content:\n\n" + e.getMessage());
|
||||||
|
Log.e("termux", "handleContentUri(uri=" + uri + ") failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void promptNameAndSave(final InputStream in, final String attachmentFileName) {
|
||||||
|
DialogUtils.textInput(this, R.string.file_received_title, attachmentFileName
|
||||||
|
, android.R.string.ok, new DialogUtils.TextSetListener() {
|
||||||
|
@Override
|
||||||
|
public void onTextSet(final String text) {
|
||||||
|
if (saveStreamWithName(in, text) == null) return;
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}, R.string.file_received_open_folder_button, new DialogUtils.TextSetListener() {
|
||||||
|
@Override
|
||||||
|
public void onTextSet(String text) {
|
||||||
|
if (saveStreamWithName(in, text) == null) return;
|
||||||
|
|
||||||
|
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE);
|
||||||
|
executeIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, TERMUX_RECEIVEDIR);
|
||||||
|
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
||||||
|
startService(executeIntent);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}, R.string.file_received_edit_button, new DialogUtils.TextSetListener() {
|
||||||
|
@Override
|
||||||
|
public void onTextSet(String text) {
|
||||||
|
File outFile = saveStreamWithName(in, text);
|
||||||
|
if (outFile == null) return;
|
||||||
|
|
||||||
|
final File editorProgramFile = new File(EDITOR_PROGRAM);
|
||||||
|
if (!editorProgramFile.isFile()) {
|
||||||
|
showErrorDialogAndQuit("The following file does not exist:\n$HOME/bin/termux-file-editor\n\n"
|
||||||
|
+ "Create this file as a script or a symlink - it will be called with the received file as only argument.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do this for the user if necessary:
|
||||||
|
editorProgramFile.setExecutable(true);
|
||||||
|
|
||||||
|
final Uri scriptUri = new Uri.Builder().scheme("file").path(EDITOR_PROGRAM).build();
|
||||||
|
|
||||||
|
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE, scriptUri);
|
||||||
|
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
||||||
|
executeIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()});
|
||||||
|
startService(executeIntent);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}, new DialogInterface.OnDismissListener() {
|
||||||
|
@Override
|
||||||
|
public void onDismiss(DialogInterface dialog) {
|
||||||
|
if (mFinishOnDismissNameDialog) finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public File saveStreamWithName(InputStream in, String attachmentFileName) {
|
||||||
|
File receiveDir = new File(TERMUX_RECEIVEDIR);
|
||||||
|
if (!receiveDir.isDirectory() && !receiveDir.mkdirs()) {
|
||||||
|
showErrorDialogAndQuit("Cannot create directory: " + receiveDir.getAbsolutePath());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final File outFile = new File(receiveDir, attachmentFileName);
|
||||||
|
try (FileOutputStream f = new FileOutputStream(outFile)) {
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
int readBytes;
|
||||||
|
while ((readBytes = in.read(buffer)) > 0) {
|
||||||
|
f.write(buffer, 0, readBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return outFile;
|
||||||
|
} catch (IOException e) {
|
||||||
|
showErrorDialogAndQuit("Error saving file:\n\n" + e);
|
||||||
|
Log.e("termux", "Error saving file", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleUrlAndFinish(final String url) {
|
||||||
|
final File urlOpenerProgramFile = new File(URL_OPENER_PROGRAM);
|
||||||
|
if (!urlOpenerProgramFile.isFile()) {
|
||||||
|
showErrorDialogAndQuit("The following file does not exist:\n$HOME/bin/termux-url-opener\n\n"
|
||||||
|
+ "Create this file as a script or a symlink - it will be called with the shared URL as only argument.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do this for the user if necessary:
|
||||||
|
urlOpenerProgramFile.setExecutable(true);
|
||||||
|
|
||||||
|
final Uri urlOpenerProgramUri = new Uri.Builder().scheme("file").path(URL_OPENER_PROGRAM).build();
|
||||||
|
|
||||||
|
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE, urlOpenerProgramUri);
|
||||||
|
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
||||||
|
executeIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, new String[]{url});
|
||||||
|
startService(executeIntent);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -127,10 +127,14 @@ public final class TerminalBuffer {
|
|||||||
mLines[externalToInternalRow(row)].mLineWrap = true;
|
mLines[externalToInternalRow(row)].mLineWrap = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean getLineWrap(int row) {
|
public boolean getLineWrap(int row) {
|
||||||
return mLines[externalToInternalRow(row)].mLineWrap;
|
return mLines[externalToInternalRow(row)].mLineWrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void clearLineWrap(int row) {
|
||||||
|
mLines[externalToInternalRow(row)].mLineWrap = false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resize the screen which this transcript backs. Currently, this only works if the number of columns does not
|
* Resize the screen which this transcript backs. Currently, this only works if the number of columns does not
|
||||||
* change or the rows expand (that is, it only works when shrinking the number of rows).
|
* change or the rows expand (that is, it only works when shrinking the number of rows).
|
||||||
|
|||||||
@@ -425,9 +425,9 @@ public final class TerminalEmulator {
|
|||||||
processCodePoint((codePoint & 0x7F) + 0x40);
|
processCodePoint((codePoint & 0x7F) + 0x40);
|
||||||
} else {
|
} else {
|
||||||
switch (Character.getType(codePoint)) {
|
switch (Character.getType(codePoint)) {
|
||||||
case Character.UNASSIGNED:
|
case Character.UNASSIGNED:
|
||||||
case Character.SURROGATE:
|
case Character.SURROGATE:
|
||||||
codePoint = UNICODE_REPLACEMENT_CHAR;
|
codePoint = UNICODE_REPLACEMENT_CHAR;
|
||||||
}
|
}
|
||||||
processCodePoint(codePoint);
|
processCodePoint(codePoint);
|
||||||
}
|
}
|
||||||
@@ -474,16 +474,28 @@ public final class TerminalEmulator {
|
|||||||
else
|
else
|
||||||
mSession.onBell();
|
mSession.onBell();
|
||||||
break;
|
break;
|
||||||
case 8: // BS
|
case 8: // Backspace (BS, ^H).
|
||||||
setCursorCol(Math.max(mLeftMargin, mCursorCol - 1));
|
if (mLeftMargin == mCursorCol) {
|
||||||
break;
|
// Jump to previous line if it was auto-wrapped.
|
||||||
case 9: // Horizontal tab - move to next tab stop, but not past edge of screen
|
int previousRow = mCursorRow - 1;
|
||||||
int nextTabStop = nextTabStop(1);
|
if (previousRow >= 0 && mScreen.getLineWrap(previousRow)) {
|
||||||
while (mCursorCol < nextTabStop) {
|
mScreen.clearLineWrap(previousRow);
|
||||||
// Emit newlines to get background color right.
|
setCursorRowCol(previousRow, mRightMargin - 1);
|
||||||
processCodePoint(' ');
|
}
|
||||||
}
|
} else {
|
||||||
break;
|
setCursorCol(mCursorCol - 1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 9: // Horizontal tab (HT, \t) - move to next tab stop, but not past edge of screen
|
||||||
|
// XXX: Should perhaps use color if writing to new cells. Try with
|
||||||
|
// printf "\033[41m\tXX\033[0m\n"
|
||||||
|
// The OSX Terminal.app colors the spaces from the tab red, but xterm does not.
|
||||||
|
// Note that Terminal.app only colors on new cells, in e.g.
|
||||||
|
// printf "\033[41m\t\r\033[42m\tXX\033[0m\n"
|
||||||
|
// the first cells are created with a red background, but when tabbing over
|
||||||
|
// them again with a green background they are not overwritten.
|
||||||
|
mCursorCol = nextTabStop(1);
|
||||||
|
break;
|
||||||
case 10: // Line feed (LF, \n).
|
case 10: // Line feed (LF, \n).
|
||||||
case 11: // Vertical tab (VT, \v).
|
case 11: // Vertical tab (VT, \v).
|
||||||
case 12: // Form feed (FF, \f).
|
case 12: // Form feed (FF, \f).
|
||||||
@@ -1331,7 +1343,7 @@ public final class TerminalEmulator {
|
|||||||
continueSequence(ESC_CSI_ARGS_ASTERIX);
|
continueSequence(ESC_CSI_ARGS_ASTERIX);
|
||||||
break;
|
break;
|
||||||
case '@': {
|
case '@': {
|
||||||
// "CSI{n}@" - Insert ${n} space characters (ICH) - http://www.vt100.net/docs/vt510-rm/ICH.
|
// "CSI{n}@" - Insert ${n} space characters (ICH) - http://www.vt100.net/docs/vt510-rm/ICH.
|
||||||
mAboutToAutoWrap = false;
|
mAboutToAutoWrap = false;
|
||||||
int columnsAfterCursor = mColumns - mCursorCol;
|
int columnsAfterCursor = mColumns - mCursorCol;
|
||||||
int spacesToInsert = Math.min(getArg0(1), columnsAfterCursor);
|
int spacesToInsert = Math.min(getArg0(1), columnsAfterCursor);
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ public final class TerminalSession extends TerminalOutput {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The file descriptor referencing the master half of a pseudo-terminal pair, resulting from calling
|
* The file descriptor referencing the master half of a pseudo-terminal pair, resulting from calling
|
||||||
* {@link JNI#createSubprocess(String, String, String[], String[], int[])}.
|
* {@link JNI#createSubprocess(String, String, String[], String[], int[], int, int)}.
|
||||||
*/
|
*/
|
||||||
private int mTerminalFileDescriptor;
|
private int mTerminalFileDescriptor;
|
||||||
|
|
||||||
|
|||||||
@@ -779,8 +779,9 @@ public final class TerminalView extends View {
|
|||||||
|
|
||||||
public void checkForFontAndColors() {
|
public void checkForFontAndColors() {
|
||||||
try {
|
try {
|
||||||
File fontFile = new File("/data/data/com.termux/files/home/.termux/font.ttf");
|
// Hard-coded paths since this file is used also in Termux:Float.
|
||||||
File colorsFile = new File("/data/data/com.termux/files/home/.termux/colors.properties");
|
@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");
|
||||||
|
|
||||||
final Properties props = new Properties();
|
final Properties props = new Properties();
|
||||||
if (colorsFile.isFile()) {
|
if (colorsFile.isFile()) {
|
||||||
@@ -896,12 +897,16 @@ public final class TerminalView extends View {
|
|||||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||||
final int[] ACTION_MODE_ATTRS = { android.R.attr.actionModeCopyDrawable, android.R.attr.actionModePasteDrawable, };
|
final int[] ACTION_MODE_ATTRS = { android.R.attr.actionModeCopyDrawable, android.R.attr.actionModePasteDrawable, };
|
||||||
TypedArray styledAttributes = getContext().obtainStyledAttributes(ACTION_MODE_ATTRS);
|
TypedArray styledAttributes = getContext().obtainStyledAttributes(ACTION_MODE_ATTRS);
|
||||||
int show = MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT;
|
try {
|
||||||
|
int show = MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT;
|
||||||
|
|
||||||
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
menu.add(Menu.NONE, 1, Menu.NONE, R.string.copy_text).setIcon(styledAttributes.getResourceId(0, 0)).setShowAsAction(show);
|
menu.add(Menu.NONE, 1, Menu.NONE, R.string.copy_text).setIcon(styledAttributes.getResourceId(0, 0)).setShowAsAction(show);
|
||||||
menu.add(Menu.NONE, 2, Menu.NONE, R.string.paste_text).setIcon(styledAttributes.getResourceId(1, 0)).setEnabled(clipboard.hasPrimaryClip()).setShowAsAction(show);
|
menu.add(Menu.NONE, 2, Menu.NONE, R.string.paste_text).setIcon(styledAttributes.getResourceId(1, 0)).setEnabled(clipboard.hasPrimaryClip()).setShowAsAction(show);
|
||||||
menu.add(Menu.NONE, 3, Menu.NONE, R.string.text_selection_more);
|
menu.add(Menu.NONE, 3, Menu.NONE, R.string.text_selection_more);
|
||||||
|
} finally {
|
||||||
|
styledAttributes.recycle();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,4 +55,8 @@
|
|||||||
|
|
||||||
<string name="empty_folder">Empty folder.</string>
|
<string name="empty_folder">Empty folder.</string>
|
||||||
|
|
||||||
|
<string name="file_received_title">Save file in ~/downloads/</string>
|
||||||
|
<string name="file_received_edit_button">Edit</string>
|
||||||
|
<string name="file_received_open_folder_button">Open folder</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -163,7 +163,12 @@ public class CursorAndScreenTest extends TerminalTestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testHorizontalTabColorsBackground() {
|
/**
|
||||||
|
* See comments on horizontal tab handling in TerminalEmulator.java.
|
||||||
|
*
|
||||||
|
* We do not want to color already written cells when tabbing over them.
|
||||||
|
*/
|
||||||
|
public void DISABLED_testHorizontalTabColorsBackground() {
|
||||||
withTerminalSized(10, 3).enterString("\033[48;5;15m").enterString("\t");
|
withTerminalSized(10, 3).enterString("\033[48;5;15m").enterString("\t");
|
||||||
assertCursorAt(0, 8);
|
assertCursorAt(0, 8);
|
||||||
for (int i = 0; i < 10; i++) {
|
for (int i = 0; i < 10; i++) {
|
||||||
@@ -213,4 +218,13 @@ public class CursorAndScreenTest extends TerminalTestCase {
|
|||||||
" -");
|
" -");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testBackspaceAcrossWrappedLines() {
|
||||||
|
// Backspace should not go to previous line if not auto-wrapped:
|
||||||
|
withTerminalSized(3, 3).enterString("hi\r\n\b\byou").assertLinesAre("hi ", "you", " ");
|
||||||
|
// Backspace should go to previous line if auto-wrapped:
|
||||||
|
withTerminalSized(3, 3).enterString("hi y").assertLinesAre("hi ", "y ", " ").enterString("\b\b#").assertLinesAre("hi#", "y ", " ");
|
||||||
|
// Initial backspace should do nothing:
|
||||||
|
withTerminalSized(3, 3).enterString("\b\b\b\bhi").assertLinesAre("hi ", " ", " ");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -258,4 +258,9 @@ public class TerminalTest extends TerminalTestCase {
|
|||||||
withTerminalSized(3, 3).enterString("abc\r ").assertLinesAre(" bc", " ", " ").assertCursorAt(0, 1);
|
withTerminalSized(3, 3).enterString("abc\r ").assertLinesAre(" bc", " ", " ").assertCursorAt(0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testTab() {
|
||||||
|
withTerminalSized(11, 2).enterString("01234567890\r\tXX").assertLinesAre("01234567XX0", " ");
|
||||||
|
withTerminalSized(11, 2).enterString("01234567890\033[44m\r\tXX").assertLinesAre("01234567XX0", " ");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
|||||||
#Wed Dec 23 01:44:47 CET 2015
|
#Tue Mar 15 00:24:33 CET 2016
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-bin.zip
|
||||||
|
|||||||
Reference in New Issue
Block a user