termux-app/app/src/main/java/com/termux/app/TermuxActivity.java

1002 lines
43 KiB
Java
Raw Normal View History

2015-10-25 14:27:32 +00:00
package com.termux.app;
import android.Manifest;
2015-10-25 14:27:32 +00:00
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
2015-10-25 14:27:32 +00:00
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
2015-10-25 14:27:32 +00:00
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.media.AudioAttributes;
import android.media.SoundPool;
2015-10-25 14:27:32 +00:00
import android.net.Uri;
import android.os.Build;
2015-10-25 14:27:32 +00:00
import android.os.Bundle;
import android.os.IBinder;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.StyleSpan;
2016-06-27 22:56:30 +00:00
import android.util.Log;
2015-10-25 14:27:32 +00:00
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.autofill.AutofillManager;
2015-10-25 14:27:32 +00:00
import android.view.inputmethod.InputMethodManager;
import android.widget.ArrayAdapter;
2016-05-20 08:44:23 +00:00
import android.widget.EditText;
2015-10-25 14:27:32 +00:00
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.termux.R;
2016-06-27 22:56:30 +00:00
import com.termux.terminal.EmulatorDebug;
import com.termux.terminal.TerminalColors;
import com.termux.terminal.TerminalSession;
import com.termux.terminal.TerminalSession.SessionChangedCallback;
2016-06-27 22:56:30 +00:00
import com.termux.terminal.TextStyle;
import com.termux.view.TerminalView;
2016-06-27 22:56:30 +00:00
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
2016-06-27 22:56:30 +00:00
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
2019-01-11 20:19:06 +00:00
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
2015-10-25 14:27:32 +00:00
/**
* A terminal emulator activity.
* <p/>
2015-10-25 14:27:32 +00:00
* See
* <ul>
* <li>http://www.mongrel-phones.com.au/default/how_to_make_a_local_service_and_bind_to_it_in_android</li>
* <li>https://code.google.com/p/android/issues/detail?id=6426</li>
* </ul>
* about memory leaks.
*/
public final class TermuxActivity extends Activity implements ServiceConnection {
public static final String TERMUX_FAILSAFE_SESSION_ACTION = "com.termux.app.failsafe_session";
private static final int CONTEXTMENU_SELECT_URL_ID = 0;
private static final int CONTEXTMENU_SHARE_TRANSCRIPT_ID = 1;
private static final int CONTEXTMENU_PASTE_ID = 3;
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_HELP_ID = 8;
2019-01-15 13:51:41 +00:00
private static final int CONTEXTMENU_TOGGLE_KEEP_SCREEN_ON = 9;
private static final int CONTEXTMENU_AUTOFILL_ID = 10;
2015-10-25 14:27:32 +00:00
private static final int MAX_SESSIONS = 8;
2015-10-25 14:27:32 +00:00
private static final int REQUESTCODE_PERMISSION_STORAGE = 1234;
private static final String RELOAD_STYLE_ACTION = "com.termux.app.reload_style";
2015-10-25 14:27:32 +00:00
private static final String BROADCAST_TERMUX_OPENED = "com.termux.app.OPENED";
/** The main view of the activity showing the terminal. Initialized in onCreate(). */
@SuppressWarnings("NullableProblems")
@NonNull
TerminalView mTerminalView;
2015-10-25 14:27:32 +00:00
2016-06-27 22:56:30 +00:00
ExtraKeysView mExtraKeysView;
TermuxPreferences mSettings;
/**
* The connection to the {@link TermuxService}. Requested in {@link #onCreate(Bundle)} with a call to
* {@link #bindService(Intent, ServiceConnection, int)}, and obtained and stored in
* {@link #onServiceConnected(ComponentName, IBinder)}.
*/
TermuxService mTermService;
/** Initialized in {@link #onServiceConnected(ComponentName, IBinder)}. */
ArrayAdapter<TerminalSession> mListViewAdapter;
/** The last toast shown, used cancel current toast before showing new in {@link #showToast(String, boolean)}. */
Toast mLastToast;
/**
* If between onResume() and onStop(). Note that only one session is in the foreground of the terminal view at the
* time, so if the session causing a change is not in the foreground it should probably be treated as background.
*/
boolean mIsVisible;
2019-10-07 15:15:17 +00:00
boolean mIsUsingBlackUI;
int mNavBarHeight;
2016-07-31 20:28:17 +00:00
final SoundPool mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
2016-07-31 20:28:17 +00:00
int mBellSoundId;
private final BroadcastReceiver mBroadcastReceiever = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (mIsVisible) {
String whatToReload = intent.getStringExtra(RELOAD_STYLE_ACTION);
if ("storage".equals(whatToReload)) {
if (ensureStoragePermissionGranted())
TermuxInstaller.setupStorageSymlinks(TermuxActivity.this);
return;
}
checkForFontAndColors();
mSettings.reloadFromProperties(TermuxActivity.this);
if (mExtraKeysView != null) {
Improvements to extra keys (#1479) * Make popup keys for extra keys row configurable This makes the keys you get when swiping up on a key configurable. You can configure such a key by using an array of strings instead of a single string in the row. The first entry will be the normal key and the second will be the extra key. This is a slightly breaking change, as people that have configured custom extra keys with "-" or "/" will have to change the config to keep the popup keys. The default config will remain the same in terms of functionality, i.e. it includes the same popup key for "-". * Make popup keys interact well with long press keys This stops the repeat action when the popup is shown, and makes sure the popup is closed when you release even if there has been some repeat actions. * Support configuring the style of the extra keys This adds a setting for choosing between the different ways to render key names that were already present in ExtraKeysView. The available setting values are "arrows-only", "arrows-all", "all", "none" and "default". Other values will fallback to "default". Can be used as a workaround for #1410 * Support using modifier keys with letter keys in extra keys This allows you to use the modifier keys on the extra keys rows, e.g. ctrl, together with another button on the extra keys rows, as long as that button is a normal letter and not a special key. Support for special keys will come in the next commit. * Support using modifier keys with special keys in extra keys This allows you to use the modifier keys on the extra keys rows together with a special key on the extra keys rows, e.g. CTRL+LEFT. Fixes #745, fixes most of #895 and possibly #154 * Support mapping extra keys to other actions This adds a setting called extra-keys-map which allows you to map a key on the extra keys rows to another action. The value is a json object where the key is the button text as configured in extra-keys and the value is the action. Multiple actions can be used, but if they are special characters (like ESC or LEFT) they have to be separated from the other characters with a space on each side. If you want an actual space character, use SPACE. For example if you want to add a key to go to the next active channel in weechat, you can use this: extra-keys-map = {"weechat next": "ESC a"} And then add "weechat next" to extra-keys. The name can of course be whatever you want. Or if you want the button for the UP arrow to show ⇧ instead of ↑, you can use this: extra-keys-map = {"⇧": "UP"} And put "⇧" in extra-keys instead of "UP". Modifier keys (ctrl, alt and shift) can't be used in this map yet. Support for ctrl and alt will come in the next commit. I think this fixes #1186 * Support CTRL and ALT in extra keys map This allows you to use CTRL and ALT in extra-keys-map. For example if you want a button to exit the terminal, you can use this: extra-keys-map = {"exit": "CTRL d"} And add "exit" to extra-keys. * Support a KEYBOARD button in extra keys This toggles showing the keyboard input method. * Support specifying macro keys in the extra-keys option Instead of specifying macros in the separate extra-keys-map option by matching the key name in the two options, you can now use "macro" instead of "key" in extra-keys, and it will be a macro, i.e. a sequence of multiple keys separated by space. * Remove option extra-keys-map Now that you can specify macro in extra-keys, there is no point in having this separate option. Instead of specifying the value to display as key, and the macro to perform in extra-keys-map, you would now specify the value to display in the display property and the macro to perform in the macro property. * Lookup display text when creating ExtraKeyButton This will make it easier to support key aliases for macros in the next commit. * Add support for a key to open the drawer Fixes (I think) #1325
2020-06-09 09:17:07 +00:00
mExtraKeysView.reload(mSettings.mExtraKeys);
}
}
}
};
2015-10-25 14:27:32 +00:00
2016-06-27 22:56:30 +00:00
void checkForFontAndColors() {
try {
@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();
if (colorsFile.isFile()) {
try (InputStream in = new FileInputStream(colorsFile)) {
props.load(in);
}
}
TerminalColors.COLOR_SCHEME.updateWith(props);
TerminalSession session = getCurrentTermSession();
if (session != null && session.getEmulator() != null) {
session.getEmulator().mColors.reset();
}
updateBackgroundColor();
final Typeface newTypeface = (fontFile.exists() && fontFile.length() > 0) ? Typeface.createFromFile(fontFile) : Typeface.MONOSPACE;
mTerminalView.setTypeface(newTypeface);
} catch (Exception e) {
Log.e(EmulatorDebug.LOG_TAG, "Error in checkForFontAndColors()", e);
}
}
void updateBackgroundColor() {
TerminalSession session = getCurrentTermSession();
if (session != null && session.getEmulator() != null) {
getWindow().getDecorView().setBackgroundColor(session.getEmulator().mColors.mCurrentColors[TextStyle.COLOR_INDEX_BACKGROUND]);
}
}
/** For processes to access shared internal storage (/sdcard) we need this permission. */
public boolean ensureStoragePermissionGranted() {
2019-11-03 21:39:21 +00:00
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
return true;
2019-11-03 21:39:21 +00:00
} else {
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUESTCODE_PERMISSION_STORAGE);
return false;
}
}
@Override
public void onCreate(Bundle bundle) {
2016-05-20 08:44:23 +00:00
mSettings = new TermuxPreferences(this);
2019-10-07 15:15:17 +00:00
mIsUsingBlackUI = mSettings.isUsingBlackUI();
if (mIsUsingBlackUI) {
this.setTheme(R.style.Theme_Termux_Black);
} else {
this.setTheme(R.style.Theme_Termux);
}
super.onCreate(bundle);
2016-05-20 08:44:23 +00:00
2016-05-20 08:44:23 +00:00
setContentView(R.layout.drawer_layout);
2019-10-07 15:15:17 +00:00
View content = findViewById(android.R.id.content);
content.setOnApplyWindowInsetsListener((v, insets) -> {
mNavBarHeight = insets.getSystemWindowInsetBottom();
return insets;
});
if (mSettings.isUsingFullScreen()) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
2019-10-07 15:15:17 +00:00
if (mIsUsingBlackUI) {
findViewById(R.id.left_drawer).setBackgroundColor(
getResources().getColor(android.R.color.background_dark)
);
}
2017-11-22 00:27:14 +00:00
mTerminalView = findViewById(R.id.terminal_view);
mTerminalView.setOnKeyListener(new TermuxViewClient(this));
2016-05-20 08:44:23 +00:00
mTerminalView.setTextSize(mSettings.getFontSize());
2019-01-15 13:51:41 +00:00
mTerminalView.setKeepScreenOn(mSettings.isScreenAlwaysOn());
mTerminalView.requestFocus();
2015-10-25 14:27:32 +00:00
2017-11-22 00:27:14 +00:00
final ViewPager viewPager = findViewById(R.id.viewpager);
if (mSettings.mShowExtraKeys) viewPager.setVisibility(View.VISIBLE);
2018-04-06 10:11:57 +00:00
ViewGroup.LayoutParams layoutParams = viewPager.getLayoutParams();
Improvements to extra keys (#1479) * Make popup keys for extra keys row configurable This makes the keys you get when swiping up on a key configurable. You can configure such a key by using an array of strings instead of a single string in the row. The first entry will be the normal key and the second will be the extra key. This is a slightly breaking change, as people that have configured custom extra keys with "-" or "/" will have to change the config to keep the popup keys. The default config will remain the same in terms of functionality, i.e. it includes the same popup key for "-". * Make popup keys interact well with long press keys This stops the repeat action when the popup is shown, and makes sure the popup is closed when you release even if there has been some repeat actions. * Support configuring the style of the extra keys This adds a setting for choosing between the different ways to render key names that were already present in ExtraKeysView. The available setting values are "arrows-only", "arrows-all", "all", "none" and "default". Other values will fallback to "default". Can be used as a workaround for #1410 * Support using modifier keys with letter keys in extra keys This allows you to use the modifier keys on the extra keys rows, e.g. ctrl, together with another button on the extra keys rows, as long as that button is a normal letter and not a special key. Support for special keys will come in the next commit. * Support using modifier keys with special keys in extra keys This allows you to use the modifier keys on the extra keys rows together with a special key on the extra keys rows, e.g. CTRL+LEFT. Fixes #745, fixes most of #895 and possibly #154 * Support mapping extra keys to other actions This adds a setting called extra-keys-map which allows you to map a key on the extra keys rows to another action. The value is a json object where the key is the button text as configured in extra-keys and the value is the action. Multiple actions can be used, but if they are special characters (like ESC or LEFT) they have to be separated from the other characters with a space on each side. If you want an actual space character, use SPACE. For example if you want to add a key to go to the next active channel in weechat, you can use this: extra-keys-map = {"weechat next": "ESC a"} And then add "weechat next" to extra-keys. The name can of course be whatever you want. Or if you want the button for the UP arrow to show ⇧ instead of ↑, you can use this: extra-keys-map = {"⇧": "UP"} And put "⇧" in extra-keys instead of "UP". Modifier keys (ctrl, alt and shift) can't be used in this map yet. Support for ctrl and alt will come in the next commit. I think this fixes #1186 * Support CTRL and ALT in extra keys map This allows you to use CTRL and ALT in extra-keys-map. For example if you want a button to exit the terminal, you can use this: extra-keys-map = {"exit": "CTRL d"} And add "exit" to extra-keys. * Support a KEYBOARD button in extra keys This toggles showing the keyboard input method. * Support specifying macro keys in the extra-keys option Instead of specifying macros in the separate extra-keys-map option by matching the key name in the two options, you can now use "macro" instead of "key" in extra-keys, and it will be a macro, i.e. a sequence of multiple keys separated by space. * Remove option extra-keys-map Now that you can specify macro in extra-keys, there is no point in having this separate option. Instead of specifying the value to display as key, and the macro to perform in extra-keys-map, you would now specify the value to display in the display property and the macro to perform in the macro property. * Lookup display text when creating ExtraKeyButton This will make it easier to support key aliases for macros in the next commit. * Add support for a key to open the drawer Fixes (I think) #1325
2020-06-09 09:17:07 +00:00
layoutParams.height = layoutParams.height * (mSettings.mExtraKeys == null ? 0 : mSettings.mExtraKeys.getMatrix().length);
2018-04-06 10:11:57 +00:00
viewPager.setLayoutParams(layoutParams);
2016-05-20 08:44:23 +00:00
viewPager.setAdapter(new PagerAdapter() {
@Override
public int getCount() {
return 2;
}
@Override
2017-11-22 00:27:14 +00:00
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
2016-05-20 08:44:23 +00:00
return view == object;
}
2017-11-22 00:27:14 +00:00
@NonNull
2016-05-20 08:44:23 +00:00
@Override
2017-11-22 00:27:14 +00:00
public Object instantiateItem(@NonNull ViewGroup collection, int position) {
2016-05-20 08:44:23 +00:00
LayoutInflater inflater = LayoutInflater.from(TermuxActivity.this);
View layout;
if (position == 0) {
2016-06-27 22:56:30 +00:00
layout = mExtraKeysView = (ExtraKeysView) inflater.inflate(R.layout.extra_keys_main, collection, false);
Improvements to extra keys (#1479) * Make popup keys for extra keys row configurable This makes the keys you get when swiping up on a key configurable. You can configure such a key by using an array of strings instead of a single string in the row. The first entry will be the normal key and the second will be the extra key. This is a slightly breaking change, as people that have configured custom extra keys with "-" or "/" will have to change the config to keep the popup keys. The default config will remain the same in terms of functionality, i.e. it includes the same popup key for "-". * Make popup keys interact well with long press keys This stops the repeat action when the popup is shown, and makes sure the popup is closed when you release even if there has been some repeat actions. * Support configuring the style of the extra keys This adds a setting for choosing between the different ways to render key names that were already present in ExtraKeysView. The available setting values are "arrows-only", "arrows-all", "all", "none" and "default". Other values will fallback to "default". Can be used as a workaround for #1410 * Support using modifier keys with letter keys in extra keys This allows you to use the modifier keys on the extra keys rows, e.g. ctrl, together with another button on the extra keys rows, as long as that button is a normal letter and not a special key. Support for special keys will come in the next commit. * Support using modifier keys with special keys in extra keys This allows you to use the modifier keys on the extra keys rows together with a special key on the extra keys rows, e.g. CTRL+LEFT. Fixes #745, fixes most of #895 and possibly #154 * Support mapping extra keys to other actions This adds a setting called extra-keys-map which allows you to map a key on the extra keys rows to another action. The value is a json object where the key is the button text as configured in extra-keys and the value is the action. Multiple actions can be used, but if they are special characters (like ESC or LEFT) they have to be separated from the other characters with a space on each side. If you want an actual space character, use SPACE. For example if you want to add a key to go to the next active channel in weechat, you can use this: extra-keys-map = {"weechat next": "ESC a"} And then add "weechat next" to extra-keys. The name can of course be whatever you want. Or if you want the button for the UP arrow to show ⇧ instead of ↑, you can use this: extra-keys-map = {"⇧": "UP"} And put "⇧" in extra-keys instead of "UP". Modifier keys (ctrl, alt and shift) can't be used in this map yet. Support for ctrl and alt will come in the next commit. I think this fixes #1186 * Support CTRL and ALT in extra keys map This allows you to use CTRL and ALT in extra-keys-map. For example if you want a button to exit the terminal, you can use this: extra-keys-map = {"exit": "CTRL d"} And add "exit" to extra-keys. * Support a KEYBOARD button in extra keys This toggles showing the keyboard input method. * Support specifying macro keys in the extra-keys option Instead of specifying macros in the separate extra-keys-map option by matching the key name in the two options, you can now use "macro" instead of "key" in extra-keys, and it will be a macro, i.e. a sequence of multiple keys separated by space. * Remove option extra-keys-map Now that you can specify macro in extra-keys, there is no point in having this separate option. Instead of specifying the value to display as key, and the macro to perform in extra-keys-map, you would now specify the value to display in the display property and the macro to perform in the macro property. * Lookup display text when creating ExtraKeyButton This will make it easier to support key aliases for macros in the next commit. * Add support for a key to open the drawer Fixes (I think) #1325
2020-06-09 09:17:07 +00:00
mExtraKeysView.reload(mSettings.mExtraKeys);
// apply extra keys fix if enabled in prefs
if (mSettings.isUsingFullScreen() && mSettings.isUsingFullScreenWorkAround()) {
FullScreenWorkAround.apply(TermuxActivity.this);
}
2016-05-20 08:44:23 +00:00
} else {
2016-06-27 22:56:30 +00:00
layout = inflater.inflate(R.layout.extra_keys_right, collection, false);
2017-11-22 00:27:14 +00:00
final EditText editText = layout.findViewById(R.id.text_input);
2018-09-28 22:49:05 +00:00
editText.setOnEditorActionListener((v, actionId, event) -> {
TerminalSession session = getCurrentTermSession();
if (session != null) {
if (session.isRunning()) {
String textToSend = editText.getText().toString();
if (textToSend.length() == 0) textToSend = "\r";
2018-09-28 22:49:05 +00:00
session.write(textToSend);
} else {
removeFinishedSession(session);
}
2018-09-28 22:49:05 +00:00
editText.setText("");
2016-05-20 08:44:23 +00:00
}
2018-09-28 22:49:05 +00:00
return true;
2016-05-20 08:44:23 +00:00
});
}
collection.addView(layout);
return layout;
}
@Override
2017-11-22 00:27:14 +00:00
public void destroyItem(@NonNull ViewGroup collection, int position, @NonNull Object view) {
2016-05-20 08:44:23 +00:00
collection.removeView((View) view);
}
});
viewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
@Override
public void onPageSelected(int position) {
if (position == 0) {
mTerminalView.requestFocus();
} else {
2017-11-22 00:27:14 +00:00
final EditText editText = viewPager.findViewById(R.id.text_input);
2016-05-20 08:44:23 +00:00
if (editText != null) editText.requestFocus();
}
}
});
View newSessionButton = findViewById(R.id.new_session_button);
2018-09-28 22:49:05 +00:00
newSessionButton.setOnClickListener(v -> addNewSession(false, null));
newSessionButton.setOnLongClickListener(v -> {
DialogUtils.textInput(TermuxActivity.this, R.string.session_new_named_title, null, R.string.session_new_named_positive_button,
text -> addNewSession(false, text), R.string.new_session_failsafe, text -> addNewSession(true, text)
, -1, null, null);
return true;
});
2018-09-28 22:49:05 +00:00
findViewById(R.id.toggle_keyboard_button).setOnClickListener(v -> {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0);
getDrawer().closeDrawers();
});
2015-10-25 14:27:32 +00:00
2018-09-28 22:49:05 +00:00
findViewById(R.id.toggle_keyboard_button).setOnLongClickListener(v -> {
toggleShowExtraKeys();
return true;
2016-05-20 08:44:23 +00:00
});
registerForContextMenu(mTerminalView);
2015-10-25 14:27:32 +00:00
Intent serviceIntent = new Intent(this, TermuxService.class);
// Start the service and make it run regardless of who is bound to it:
startService(serviceIntent);
if (!bindService(serviceIntent, this, 0))
throw new RuntimeException("bindService() failed");
2015-10-25 14:27:32 +00:00
checkForFontAndColors();
mBellSoundId = mBellSoundPool.load(this, R.raw.bell, 1);
sendOpenedBroadcast();
}
public int getNavBarHeight() {
return mNavBarHeight;
}
/**
* Send a broadcast notifying Termux app has been opened
*/
void sendOpenedBroadcast() {
Intent broadcast = new Intent(BROADCAST_TERMUX_OPENED);
List<ResolveInfo> matches = getPackageManager().queryBroadcastReceivers(broadcast, 0);
// send broadcast to registered Termux receivers
// this technique is needed to work around broadcast changes that Oreo introduced
for (ResolveInfo info : matches) {
Intent explicitBroadcast = new Intent(broadcast);
ComponentName cname = new ComponentName(info.activityInfo.applicationInfo.packageName,
info.activityInfo.name);
explicitBroadcast.setComponent(cname);
sendBroadcast(explicitBroadcast);
}
}
2015-10-25 14:27:32 +00:00
2016-06-27 22:56:30 +00:00
void toggleShowExtraKeys() {
2017-11-22 00:27:14 +00:00
final ViewPager viewPager = findViewById(R.id.viewpager);
2016-06-27 22:56:30 +00:00
final boolean showNow = mSettings.toggleShowExtraKeys(TermuxActivity.this);
viewPager.setVisibility(showNow ? View.VISIBLE : View.GONE);
if (showNow && viewPager.getCurrentItem() == 1) {
// Focus the text input view if just revealed.
findViewById(R.id.text_input).requestFocus();
}
}
/**
* Part of the {@link ServiceConnection} interface. The service is bound with
* {@link #bindService(Intent, ServiceConnection, int)} in {@link #onCreate(Bundle)} which will cause a call to this
* callback method.
*/
@Override
public void onServiceConnected(ComponentName componentName, IBinder service) {
mTermService = ((TermuxService.LocalBinder) service).service;
mTermService.mSessionChangeCallback = new SessionChangedCallback() {
@Override
public void onTextChanged(TerminalSession changedSession) {
if (!mIsVisible) return;
if (getCurrentTermSession() == changedSession) mTerminalView.onScreenUpdated();
}
@Override
public void onTitleChanged(TerminalSession updatedSession) {
if (!mIsVisible) return;
if (updatedSession != getCurrentTermSession()) {
// Only show toast for other sessions than the current one, since the user
// probably consciously caused the title change to change in the current session
// and don't want an annoying toast for that.
showToast(toToastTitle(updatedSession), false);
}
mListViewAdapter.notifyDataSetChanged();
}
@Override
public void onSessionFinished(final TerminalSession finishedSession) {
if (mTermService.mWantsToStop) {
// The service wants to stop as soon as possible.
finish();
return;
}
2016-05-20 08:44:23 +00:00
if (mIsVisible && finishedSession != getCurrentTermSession()) {
// Show toast for non-current sessions that exit.
int indexOfSession = mTermService.getSessions().indexOf(finishedSession);
// Verify that session was not removed before we got told about it finishing:
if (indexOfSession >= 0)
showToast(toToastTitle(finishedSession) + " - exited", true);
}
if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
// On Android TV devices we need to use older behaviour because we may
// not be able to have multiple launcher icons.
if (mTermService.getSessions().size() > 1) {
removeFinishedSession(finishedSession);
}
} else {
// Once we have a separate launcher icon for the failsafe session, it
// should be safe to auto-close session on exit code '0' or '130'.
if (finishedSession.getExitStatus() == 0 || finishedSession.getExitStatus() == 130) {
removeFinishedSession(finishedSession);
}
}
mListViewAdapter.notifyDataSetChanged();
}
@Override
public void onClipboardText(TerminalSession session, String text) {
if (!mIsVisible) return;
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(text)));
}
@Override
public void onBell(TerminalSession session) {
if (!mIsVisible) return;
switch (mSettings.mBellBehaviour) {
case TermuxPreferences.BELL_BEEP:
mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
break;
case TermuxPreferences.BELL_VIBRATE:
BellUtil.getInstance(TermuxActivity.this).doBell();
break;
case TermuxPreferences.BELL_IGNORE:
// Ignore the bell character.
break;
}
}
2016-06-27 22:56:30 +00:00
@Override
public void onColorsChanged(TerminalSession changedSession) {
if (getCurrentTermSession() == changedSession) updateBackgroundColor();
}
};
2015-10-25 14:27:32 +00:00
2017-11-22 00:27:14 +00:00
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);
@NonNull
@Override
2016-10-15 22:12:42 +00:00
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
View row = convertView;
if (row == null) {
LayoutInflater inflater = getLayoutInflater();
row = inflater.inflate(R.layout.line_in_drawer, parent, false);
}
TerminalSession sessionAtRow = getItem(position);
boolean sessionRunning = sessionAtRow.isRunning();
2017-11-22 00:27:14 +00:00
TextView firstLineView = row.findViewById(R.id.row_line);
2019-10-07 15:15:17 +00:00
if (mIsUsingBlackUI) {
firstLineView.setBackground(
getResources().getDrawable(R.drawable.selected_session_background_black)
);
}
String name = sessionAtRow.mSessionName;
String sessionTitle = sessionAtRow.getTitle();
String numberPart = "[" + (position + 1) + "] ";
String sessionNamePart = (TextUtils.isEmpty(name) ? "" : name);
String sessionTitlePart = (TextUtils.isEmpty(sessionTitle) ? "" : ((sessionNamePart.isEmpty() ? "" : "\n") + sessionTitle));
String text = numberPart + sessionNamePart + sessionTitlePart;
SpannableString styledText = new SpannableString(text);
styledText.setSpan(boldSpan, 0, numberPart.length() + sessionNamePart.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
styledText.setSpan(italicSpan, numberPart.length() + sessionNamePart.length(), text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
firstLineView.setText(styledText);
if (sessionRunning) {
firstLineView.setPaintFlags(firstLineView.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
} else {
firstLineView.setPaintFlags(firstLineView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
}
2019-10-07 15:15:17 +00:00
int defaultColor = mIsUsingBlackUI ? Color.WHITE : Color.BLACK;
int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? defaultColor : Color.RED;
firstLineView.setTextColor(color);
return row;
}
};
listView.setAdapter(mListViewAdapter);
2018-09-28 22:49:05 +00:00
listView.setOnItemClickListener((parent, view, position, id) -> {
TerminalSession clickedSession = mListViewAdapter.getItem(position);
switchToSession(clickedSession);
getDrawer().closeDrawers();
});
2018-09-28 22:49:05 +00:00
listView.setOnItemLongClickListener((parent, view, position, id) -> {
final TerminalSession selectedSession = mListViewAdapter.getItem(position);
renameSession(selectedSession);
return true;
});
if (mTermService.getSessions().isEmpty()) {
if (mIsVisible) {
2018-09-28 22:49:05 +00:00
TermuxInstaller.setupIfNeeded(TermuxActivity.this, () -> {
if (mTermService == null) return; // Activity might have been destroyed.
try {
Bundle bundle = getIntent().getExtras();
boolean launchFailsafe = false;
if (bundle != null) {
launchFailsafe = bundle.getBoolean(TERMUX_FAILSAFE_SESSION_ACTION, false);
}
addNewSession(launchFailsafe, null);
2018-09-28 22:49:05 +00:00
} catch (WindowManager.BadTokenException e) {
// Activity finished - ignore.
}
});
} else {
// The service connected while not in foreground - just bail out.
finish();
}
} else {
Intent i = getIntent();
2016-12-27 09:42:41 +00:00
if (i != null && Intent.ACTION_RUN.equals(i.getAction())) {
// Android 7.1 app shortcut from res/xml/shortcuts.xml.
boolean failSafe = i.getBooleanExtra(TERMUX_FAILSAFE_SESSION_ACTION, false);
addNewSession(failSafe, null);
} else {
switchToSession(getStoredCurrentSessionOrLast());
}
}
}
2015-10-25 14:27:32 +00:00
2016-06-27 22:56:30 +00:00
public void switchToSession(boolean forward) {
TerminalSession currentSession = getCurrentTermSession();
int index = mTermService.getSessions().indexOf(currentSession);
if (forward) {
if (++index >= mTermService.getSessions().size()) index = 0;
} else {
if (--index < 0) index = mTermService.getSessions().size() - 1;
}
switchToSession(mTermService.getSessions().get(index));
}
@SuppressLint("InflateParams")
void renameSession(final TerminalSession sessionToRename) {
2018-09-28 22:49:05 +00:00
DialogUtils.textInput(this, R.string.session_rename_title, sessionToRename.mSessionName, R.string.session_rename_positive_button, text -> {
sessionToRename.mSessionName = text;
mListViewAdapter.notifyDataSetChanged();
}, -1, null, -1, null, null);
}
2015-10-25 14:27:32 +00:00
@Override
public void onServiceDisconnected(ComponentName name) {
2017-11-22 00:27:14 +00:00
// Respect being stopped from the TermuxService notification action.
finish();
}
@Nullable
TerminalSession getCurrentTermSession() {
return mTerminalView.getCurrentSession();
}
@Override
public void onStart() {
super.onStart();
mIsVisible = true;
if (mTermService != null) {
// The service has connected, but data may have changed since we were last in the foreground.
switchToSession(getStoredCurrentSessionOrLast());
mListViewAdapter.notifyDataSetChanged();
}
registerReceiver(mBroadcastReceiever, new IntentFilter(RELOAD_STYLE_ACTION));
// The current terminal session may have changed while being away, force
// a refresh of the displayed terminal:
mTerminalView.onScreenUpdated();
}
@Override
protected void onStop() {
super.onStop();
mIsVisible = false;
TerminalSession currentSession = getCurrentTermSession();
if (currentSession != null) TermuxPreferences.storeCurrentSession(this, currentSession);
unregisterReceiver(mBroadcastReceiever);
getDrawer().closeDrawers();
}
@Override
public void onBackPressed() {
if (getDrawer().isDrawerOpen(Gravity.LEFT)) {
2016-05-20 08:44:23 +00:00
getDrawer().closeDrawers();
} else {
finish();
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (mTermService != null) {
// Do not leave service with references to activity.
mTermService.mSessionChangeCallback = null;
mTermService = null;
}
unbindService(this);
}
DrawerLayout getDrawer() {
return (DrawerLayout) findViewById(R.id.drawer_layout);
}
void addNewSession(boolean failSafe, String sessionName) {
if (mTermService.getSessions().size() >= MAX_SESSIONS) {
new AlertDialog.Builder(this).setTitle(R.string.max_terminals_reached_title).setMessage(R.string.max_terminals_reached_message)
.setPositiveButton(android.R.string.ok, null).show();
} else {
TerminalSession currentSession = getCurrentTermSession();
String workingDirectory;
if (currentSession == null) {
workingDirectory = mSettings.mDefaultWorkingDir;
} else {
workingDirectory = currentSession.getCwd();
}
TerminalSession newSession = mTermService.createTermSession(null, null, workingDirectory, failSafe);
if (sessionName != null) {
newSession.mSessionName = sessionName;
}
switchToSession(newSession);
getDrawer().closeDrawers();
}
}
/** Try switching to session and note about it, but do nothing if already displaying the session. */
void switchToSession(TerminalSession session) {
if (mTerminalView.attachSession(session)) {
2016-06-27 22:56:30 +00:00
noteSessionInfo();
updateBackgroundColor();
}
}
String toToastTitle(TerminalSession session) {
final int indexOfSession = mTermService.getSessions().indexOf(session);
StringBuilder toastTitle = new StringBuilder("[" + (indexOfSession + 1) + "]");
if (!TextUtils.isEmpty(session.mSessionName)) {
toastTitle.append(" ").append(session.mSessionName);
}
String title = session.getTitle();
if (!TextUtils.isEmpty(title)) {
// Space to "[${NR}] or newline after session name:
toastTitle.append(session.mSessionName == null ? " " : "\n");
toastTitle.append(title);
}
return toastTitle.toString();
}
void noteSessionInfo() {
if (!mIsVisible) return;
TerminalSession session = getCurrentTermSession();
final int indexOfSession = mTermService.getSessions().indexOf(session);
showToast(toToastTitle(session), false);
mListViewAdapter.notifyDataSetChanged();
2017-11-22 00:27:14 +00:00
final ListView lv = findViewById(R.id.left_drawer_list);
lv.setItemChecked(indexOfSession, true);
lv.smoothScrollToPosition(indexOfSession);
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
TerminalSession currentSession = getCurrentTermSession();
if (currentSession == null) return;
menu.add(Menu.NONE, CONTEXTMENU_SELECT_URL_ID, Menu.NONE, R.string.select_url);
menu.add(Menu.NONE, CONTEXTMENU_SHARE_TRANSCRIPT_ID, Menu.NONE, R.string.select_all_and_share);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillManager autofillManager = getSystemService(AutofillManager.class);
if (autofillManager != null && autofillManager.isEnabled()) {
menu.add(Menu.NONE, CONTEXTMENU_AUTOFILL_ID, Menu.NONE, R.string.autofill_password);
}
}
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_STYLING_ID, Menu.NONE, R.string.style_terminal);
2019-01-15 13:51:41 +00:00
menu.add(Menu.NONE, CONTEXTMENU_TOGGLE_KEEP_SCREEN_ON, Menu.NONE, R.string.toggle_keep_screen_on).setCheckable(true).setChecked(mSettings.isScreenAlwaysOn());
menu.add(Menu.NONE, CONTEXTMENU_HELP_ID, Menu.NONE, R.string.help);
}
/** Hook system menu to show context menu instead. */
@Override
public boolean onCreateOptionsMenu(Menu menu) {
mTerminalView.showContextMenu();
return false;
}
static LinkedHashSet<CharSequence> extractUrls(String text) {
StringBuilder regex_sb = new StringBuilder();
regex_sb.append("("); // Begin first matching group.
regex_sb.append("(?:"); // Begin scheme group.
regex_sb.append("dav|"); // The DAV proto.
regex_sb.append("dict|"); // The DICT proto.
regex_sb.append("dns|"); // The DNS proto.
regex_sb.append("file|"); // File path.
regex_sb.append("finger|"); // The Finger proto.
regex_sb.append("ftp(?:s?)|"); // The FTP proto.
regex_sb.append("git|"); // The Git proto.
regex_sb.append("gopher|"); // The Gopher proto.
regex_sb.append("http(?:s?)|"); // The HTTP proto.
regex_sb.append("imap(?:s?)|"); // The IMAP proto.
regex_sb.append("irc(?:[6s]?)|"); // The IRC proto.
regex_sb.append("ip[fn]s|"); // The IPFS proto.
regex_sb.append("ldap(?:s?)|"); // The LDAP proto.
regex_sb.append("pop3(?:s?)|"); // The POP3 proto.
regex_sb.append("redis(?:s?)|"); // The Redis proto.
regex_sb.append("rsync|"); // The Rsync proto.
regex_sb.append("rtsp(?:[su]?)|"); // The RTSP proto.
regex_sb.append("sftp|"); // The SFTP proto.
regex_sb.append("smb(?:s?)|"); // The SAMBA proto.
regex_sb.append("smtp(?:s?)|"); // The SMTP proto.
regex_sb.append("svn(?:(?:\\+ssh)?)|"); // The Subversion proto.
regex_sb.append("tcp|"); // The TCP proto.
regex_sb.append("telnet|"); // The Telnet proto.
regex_sb.append("tftp|"); // The TFTP proto.
regex_sb.append("udp|"); // The UDP proto.
regex_sb.append("vnc|"); // The VNC proto.
regex_sb.append("ws(?:s?)"); // The Websocket proto.
regex_sb.append(")://"); // End scheme group.
regex_sb.append(")"); // End first matching group.
// Begin second matching group.
regex_sb.append("(");
// User name and/or password in format 'user:pass@'.
regex_sb.append("(?:\\S+(?::\\S*)?@)?");
// Begin host group.
regex_sb.append("(?:");
// IP address (from http://www.regular-expressions.info/examples.html).
regex_sb.append("(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|");
// Host name or domain.
regex_sb.append("(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)(?:(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))?|");
// Just path. Used in case of 'file://' scheme.
regex_sb.append("/(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)");
// End host group.
regex_sb.append(")");
// Port number.
regex_sb.append("(?::\\d{1,5})?");
// Resource path with optional query string.
regex_sb.append("(?:/[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
// Fragment.
regex_sb.append("(?:#[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
// End second matching group.
regex_sb.append(")");
final Pattern urlPattern = Pattern.compile(
regex_sb.toString(),
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
LinkedHashSet<CharSequence> urlSet = new LinkedHashSet<>();
Matcher matcher = urlPattern.matcher(text);
while (matcher.find()) {
int matchStart = matcher.start(1);
int matchEnd = matcher.end();
String url = text.substring(matchStart, matchEnd);
urlSet.add(url);
}
return urlSet;
}
void showUrlSelection() {
String text = getCurrentTermSession().getEmulator().getScreen().getTranscriptTextWithFullLinesJoined();
LinkedHashSet<CharSequence> urlSet = extractUrls(text);
if (urlSet.isEmpty()) {
new AlertDialog.Builder(this).setMessage(R.string.select_url_no_found).show();
return;
}
2018-11-10 22:16:54 +00:00
final CharSequence[] urls = urlSet.toArray(new CharSequence[0]);
Collections.reverse(Arrays.asList(urls)); // Latest first.
// Click to copy url to clipboard:
2018-09-28 22:49:05 +00:00
final AlertDialog dialog = new AlertDialog.Builder(TermuxActivity.this).setItems(urls, (di, which) -> {
String url = (String) urls[which];
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(url)));
Toast.makeText(TermuxActivity.this, R.string.select_url_copied_to_clipboard, Toast.LENGTH_LONG).show();
}).setTitle(R.string.select_url_dialog_title).create();
// Long press to open URL:
2018-09-28 22:49:05 +00:00
dialog.setOnShowListener(di -> {
ListView lv = dialog.getListView(); // this is a ListView with your "buds" in it
lv.setOnItemLongClickListener((parent, view, position, id) -> {
dialog.dismiss();
String url = (String) urls[position];
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
try {
startActivity(i, null);
} catch (ActivityNotFoundException e) {
// If no applications match, Android displays a system message.
startActivity(Intent.createChooser(i, null));
}
return true;
});
});
dialog.show();
}
@Override
public boolean onContextItemSelected(MenuItem item) {
TerminalSession session = getCurrentTermSession();
switch (item.getItemId()) {
case CONTEXTMENU_SELECT_URL_ID:
showUrlSelection();
return true;
case CONTEXTMENU_SHARE_TRANSCRIPT_ID:
if (session != null) {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
String transcriptText = session.getEmulator().getScreen().getTranscriptTextWithoutJoinedLines().trim();
// See https://github.com/termux/termux-app/issues/1166.
final int MAX_LENGTH = 100_000;
if (transcriptText.length() > MAX_LENGTH) {
int cutOffIndex = transcriptText.length() - MAX_LENGTH;
int nextNewlineIndex = transcriptText.indexOf('\n', cutOffIndex);
if (nextNewlineIndex != -1 && nextNewlineIndex != transcriptText.length() - 1) {
cutOffIndex = nextNewlineIndex + 1;
}
transcriptText = transcriptText.substring(cutOffIndex).trim();
}
intent.putExtra(Intent.EXTRA_TEXT, transcriptText);
intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_transcript_title));
startActivity(Intent.createChooser(intent, getString(R.string.share_transcript_chooser_title)));
}
return true;
case CONTEXTMENU_PASTE_ID:
doPaste();
return true;
case CONTEXTMENU_KILL_PROCESS_ID:
final AlertDialog.Builder b = new AlertDialog.Builder(this);
b.setIcon(android.R.drawable.ic_dialog_alert);
b.setMessage(R.string.confirm_kill_process);
2018-09-28 22:49:05 +00:00
b.setPositiveButton(android.R.string.yes, (dialog, id) -> {
dialog.dismiss();
getCurrentTermSession().finishIfRunning();
});
b.setNegativeButton(android.R.string.no, null);
b.show();
return true;
case CONTEXTMENU_RESET_TERMINAL_ID: {
if (session != null) {
session.reset();
showToast(getResources().getString(R.string.reset_toast_notification), true);
}
return true;
}
case CONTEXTMENU_STYLING_ID: {
Intent stylingIntent = new Intent();
stylingIntent.setClassName("com.termux.styling", "com.termux.styling.TermuxStyleActivity");
try {
startActivity(stylingIntent);
} catch (ActivityNotFoundException | IllegalArgumentException e) {
// The startActivity() call is not documented to throw IllegalArgumentException.
// However, crash reporting shows that it sometimes does, so catch it here.
new AlertDialog.Builder(this).setMessage(R.string.styling_not_installed)
2020-12-30 01:35:45 +00:00
.setPositiveButton(R.string.styling_install, (dialog, which) -> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://f-droid.org/en/packages/com.termux.styling/")))).setNegativeButton(android.R.string.cancel, null).show();
}
return true;
}
case CONTEXTMENU_HELP_ID:
startActivity(new Intent(this, TermuxHelpActivity.class));
return true;
2019-01-15 13:51:41 +00:00
case CONTEXTMENU_TOGGLE_KEEP_SCREEN_ON: {
if(mTerminalView.getKeepScreenOn()) {
mTerminalView.setKeepScreenOn(false);
mSettings.setScreenAlwaysOn(this, false);
} else {
mTerminalView.setKeepScreenOn(true);
mSettings.setScreenAlwaysOn(this, true);
}
return true;
}
case CONTEXTMENU_AUTOFILL_ID: {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillManager autofillManager = getSystemService(AutofillManager.class);
if (autofillManager != null && autofillManager.isEnabled()) {
autofillManager.requestAutofill(mTerminalView);
}
}
}
default:
return super.onContextItemSelected(item);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
if (requestCode == REQUESTCODE_PERMISSION_STORAGE && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
TermuxInstaller.setupStorageSymlinks(this);
}
}
void changeFontSize(boolean increase) {
mSettings.changeFontSize(this, increase);
mTerminalView.setTextSize(mSettings.getFontSize());
}
void doPaste() {
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboard.getPrimaryClip();
if (clipData == null) return;
CharSequence paste = clipData.getItemAt(0).coerceToText(this);
if (!TextUtils.isEmpty(paste))
getCurrentTermSession().getEmulator().paste(paste.toString());
}
/** The current session as stored or the last one if that does not exist. */
public TerminalSession getStoredCurrentSessionOrLast() {
TerminalSession stored = TermuxPreferences.getCurrentSession(this);
if (stored != null) return stored;
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. */
void showToast(String text, boolean longDuration) {
if (mLastToast != null) mLastToast.cancel();
mLastToast = Toast.makeText(TermuxActivity.this, text, longDuration ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT);
mLastToast.setGravity(Gravity.TOP, 0, 0);
mLastToast.show();
}
2015-10-25 14:27:32 +00:00
public void removeFinishedSession(TerminalSession finishedSession) {
// Return pressed with finished session - remove it.
TermuxService service = mTermService;
int index = service.removeTermSession(finishedSession);
mListViewAdapter.notifyDataSetChanged();
if (mTermService.getSessions().isEmpty()) {
// There are no sessions to show, so finish the activity.
finish();
} else {
if (index >= service.getSessions().size()) {
index = service.getSessions().size() - 1;
}
switchToSession(service.getSessions().get(index));
}
}
2015-10-25 14:27:32 +00:00
}