mirror of https://github.com/termux/termux-app
967 lines
36 KiB
Java
967 lines
36 KiB
Java
package com.termux.app;
|
|
|
|
import android.annotation.SuppressLint;
|
|
import android.app.AlertDialog;
|
|
import android.content.ActivityNotFoundException;
|
|
import android.content.BroadcastReceiver;
|
|
import android.content.ComponentName;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.IntentFilter;
|
|
import android.content.ServiceConnection;
|
|
import android.net.Uri;
|
|
import android.os.Build;
|
|
import android.os.Bundle;
|
|
import android.os.IBinder;
|
|
import android.view.ContextMenu;
|
|
import android.view.ContextMenu.ContextMenuInfo;
|
|
import android.view.Gravity;
|
|
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;
|
|
import android.widget.EditText;
|
|
import android.widget.ImageButton;
|
|
import android.widget.ListView;
|
|
import android.widget.RelativeLayout;
|
|
import android.widget.Toast;
|
|
|
|
import com.termux.R;
|
|
import com.termux.app.terminal.TermuxActivityRootView;
|
|
import com.termux.shared.activities.ReportActivity;
|
|
import com.termux.shared.activity.ActivityUtils;
|
|
import com.termux.shared.activity.media.AppCompatActivityUtils;
|
|
import com.termux.shared.data.IntentUtils;
|
|
import com.termux.shared.android.PermissionUtils;
|
|
import com.termux.shared.termux.TermuxConstants;
|
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
|
|
import com.termux.app.activities.HelpActivity;
|
|
import com.termux.app.activities.SettingsActivity;
|
|
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
|
|
import com.termux.app.terminal.TermuxSessionsListViewController;
|
|
import com.termux.app.terminal.io.TerminalToolbarViewPager;
|
|
import com.termux.app.terminal.TermuxTerminalSessionClient;
|
|
import com.termux.app.terminal.TermuxTerminalViewClient;
|
|
import com.termux.shared.termux.extrakeys.ExtraKeysView;
|
|
import com.termux.app.settings.properties.TermuxAppSharedProperties;
|
|
import com.termux.shared.termux.interact.TextInputDialogUtils;
|
|
import com.termux.shared.logger.Logger;
|
|
import com.termux.shared.termux.TermuxUtils;
|
|
import com.termux.shared.termux.theme.TermuxThemeUtils;
|
|
import com.termux.shared.theme.NightMode;
|
|
import com.termux.shared.view.ViewUtils;
|
|
import com.termux.terminal.TerminalSession;
|
|
import com.termux.terminal.TerminalSessionClient;
|
|
import com.termux.app.utils.CrashUtils;
|
|
import com.termux.view.TerminalView;
|
|
import com.termux.view.TerminalViewClient;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.appcompat.app.AppCompatActivity;
|
|
import androidx.drawerlayout.widget.DrawerLayout;
|
|
import androidx.viewpager.widget.ViewPager;
|
|
|
|
import java.util.Arrays;
|
|
|
|
/**
|
|
* A terminal emulator activity.
|
|
* <p/>
|
|
* 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 AppCompatActivity implements ServiceConnection {
|
|
|
|
/**
|
|
* 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 mTermuxService;
|
|
|
|
/**
|
|
* The {@link TerminalView} shown in {@link TermuxActivity} that displays the terminal.
|
|
*/
|
|
TerminalView mTerminalView;
|
|
|
|
/**
|
|
* The {@link TerminalViewClient} interface implementation to allow for communication between
|
|
* {@link TerminalView} and {@link TermuxActivity}.
|
|
*/
|
|
TermuxTerminalViewClient mTermuxTerminalViewClient;
|
|
|
|
/**
|
|
* The {@link TerminalSessionClient} interface implementation to allow for communication between
|
|
* {@link TerminalSession} and {@link TermuxActivity}.
|
|
*/
|
|
TermuxTerminalSessionClient mTermuxTerminalSessionClient;
|
|
|
|
/**
|
|
* Termux app shared preferences manager.
|
|
*/
|
|
private TermuxAppSharedPreferences mPreferences;
|
|
|
|
/**
|
|
* Termux app shared properties manager, loaded from termux.properties
|
|
*/
|
|
private TermuxAppSharedProperties mProperties;
|
|
|
|
/**
|
|
* The root view of the {@link TermuxActivity}.
|
|
*/
|
|
TermuxActivityRootView mTermuxActivityRootView;
|
|
|
|
/**
|
|
* The space at the bottom of {@link @mTermuxActivityRootView} of the {@link TermuxActivity}.
|
|
*/
|
|
View mTermuxActivityBottomSpaceView;
|
|
|
|
/**
|
|
* The terminal extra keys view.
|
|
*/
|
|
ExtraKeysView mExtraKeysView;
|
|
|
|
/**
|
|
* The termux sessions list controller.
|
|
*/
|
|
TermuxSessionsListViewController mTermuxSessionListViewController;
|
|
|
|
/**
|
|
* The {@link TermuxActivity} broadcast receiver for various things like terminal style configuration changes.
|
|
*/
|
|
private final BroadcastReceiver mTermuxActivityBroadcastReceiver = new TermuxActivityBroadcastReceiver();
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
private boolean mIsVisible;
|
|
|
|
/**
|
|
* If onResume() was called after onCreate().
|
|
*/
|
|
private boolean isOnResumeAfterOnCreate = false;
|
|
|
|
/**
|
|
* If activity was restarted like due to call to {@link #recreate()} after receiving
|
|
* {@link TERMUX_ACTIVITY#ACTION_RELOAD_STYLE}, system dark night mode was changed or activity
|
|
* was killed by android.
|
|
*/
|
|
private boolean mIsActivityRecreated = false;
|
|
|
|
/**
|
|
* The {@link TermuxActivity} is in an invalid state and must not be run.
|
|
*/
|
|
private boolean mIsInvalidState;
|
|
|
|
private int mNavBarHeight;
|
|
|
|
private int mTerminalToolbarDefaultHeight;
|
|
|
|
|
|
private static final int CONTEXT_MENU_SELECT_URL_ID = 0;
|
|
private static final int CONTEXT_MENU_SHARE_TRANSCRIPT_ID = 1;
|
|
private static final int CONTEXT_MENU_AUTOFILL_ID = 2;
|
|
private static final int CONTEXT_MENU_RESET_TERMINAL_ID = 3;
|
|
private static final int CONTEXT_MENU_KILL_PROCESS_ID = 4;
|
|
private static final int CONTEXT_MENU_STYLING_ID = 5;
|
|
private static final int CONTEXT_MENU_TOGGLE_KEEP_SCREEN_ON = 6;
|
|
private static final int CONTEXT_MENU_HELP_ID = 7;
|
|
private static final int CONTEXT_MENU_SETTINGS_ID = 8;
|
|
private static final int CONTEXT_MENU_REPORT_ID = 9;
|
|
|
|
private static final String ARG_TERMINAL_TOOLBAR_TEXT_INPUT = "terminal_toolbar_text_input";
|
|
private static final String ARG_ACTIVITY_RECREATED = "activity_recreated";
|
|
|
|
private static final String LOG_TAG = "TermuxActivity";
|
|
|
|
@Override
|
|
public void onCreate(Bundle savedInstanceState) {
|
|
Logger.logDebug(LOG_TAG, "onCreate");
|
|
isOnResumeAfterOnCreate = true;
|
|
|
|
if (savedInstanceState != null)
|
|
mIsActivityRecreated = savedInstanceState.getBoolean(ARG_ACTIVITY_RECREATED, false);
|
|
|
|
// Check if a crash happened on last run of the app and show a
|
|
// notification with the crash details if it did
|
|
CrashUtils.notifyAppCrashOnLastRun(this, LOG_TAG);
|
|
|
|
// Delete ReportInfo serialized object files from cache older than 14 days
|
|
ReportActivity.deleteReportInfoFilesOlderThanXDays(this, 14, false);
|
|
|
|
// Load termux shared properties
|
|
mProperties = new TermuxAppSharedProperties(this);
|
|
|
|
setActivityTheme();
|
|
|
|
super.onCreate(savedInstanceState);
|
|
|
|
setContentView(R.layout.activity_termux);
|
|
|
|
// Load termux shared preferences
|
|
// This will also fail if TermuxConstants.TERMUX_PACKAGE_NAME does not equal applicationId
|
|
mPreferences = TermuxAppSharedPreferences.build(this, true);
|
|
if (mPreferences == null) {
|
|
// An AlertDialog should have shown to kill the app, so we don't continue running activity code
|
|
mIsInvalidState = true;
|
|
return;
|
|
}
|
|
|
|
setMargins();
|
|
|
|
mTermuxActivityRootView = findViewById(R.id.activity_termux_root_view);
|
|
mTermuxActivityRootView.setActivity(this);
|
|
mTermuxActivityBottomSpaceView = findViewById(R.id.activity_termux_bottom_space_view);
|
|
mTermuxActivityRootView.setOnApplyWindowInsetsListener(new TermuxActivityRootView.WindowInsetsListener());
|
|
|
|
View content = findViewById(android.R.id.content);
|
|
content.setOnApplyWindowInsetsListener((v, insets) -> {
|
|
mNavBarHeight = insets.getSystemWindowInsetBottom();
|
|
return insets;
|
|
});
|
|
|
|
if (mProperties.isUsingFullScreen()) {
|
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
|
}
|
|
|
|
setTermuxTerminalViewAndClients();
|
|
|
|
setTerminalToolbarView(savedInstanceState);
|
|
|
|
setSettingsButtonView();
|
|
|
|
setNewSessionButtonView();
|
|
|
|
setToggleKeyboardView();
|
|
|
|
registerForContextMenu(mTerminalView);
|
|
|
|
// Start the {@link TermuxService} and make it run regardless of who is bound to it
|
|
Intent serviceIntent = new Intent(this, TermuxService.class);
|
|
startService(serviceIntent);
|
|
|
|
// Attempt to bind to the service, this will call the {@link #onServiceConnected(ComponentName, IBinder)}
|
|
// callback if it succeeds.
|
|
if (!bindService(serviceIntent, this, 0))
|
|
throw new RuntimeException("bindService() failed");
|
|
|
|
// Send the {@link TermuxConstants#BROADCAST_TERMUX_OPENED} broadcast to notify apps that Termux
|
|
// app has been opened.
|
|
TermuxUtils.sendTermuxOpenedBroadcast(this);
|
|
}
|
|
|
|
@Override
|
|
public void onStart() {
|
|
super.onStart();
|
|
|
|
Logger.logDebug(LOG_TAG, "onStart");
|
|
|
|
if (mIsInvalidState) return;
|
|
|
|
mIsVisible = true;
|
|
|
|
if (mTermuxTerminalSessionClient != null)
|
|
mTermuxTerminalSessionClient.onStart();
|
|
|
|
if (mTermuxTerminalViewClient != null)
|
|
mTermuxTerminalViewClient.onStart();
|
|
|
|
if (mPreferences.isTerminalMarginAdjustmentEnabled())
|
|
addTermuxActivityRootViewGlobalLayoutListener();
|
|
|
|
registerTermuxActivityBroadcastReceiver();
|
|
}
|
|
|
|
@Override
|
|
public void onResume() {
|
|
super.onResume();
|
|
|
|
Logger.logVerbose(LOG_TAG, "onResume");
|
|
|
|
if (mIsInvalidState) return;
|
|
|
|
if (mTermuxTerminalSessionClient != null)
|
|
mTermuxTerminalSessionClient.onResume();
|
|
|
|
if (mTermuxTerminalViewClient != null)
|
|
mTermuxTerminalViewClient.onResume();
|
|
|
|
isOnResumeAfterOnCreate = false;
|
|
}
|
|
|
|
@Override
|
|
protected void onStop() {
|
|
super.onStop();
|
|
|
|
Logger.logDebug(LOG_TAG, "onStop");
|
|
|
|
if (mIsInvalidState) return;
|
|
|
|
mIsVisible = false;
|
|
|
|
if (mTermuxTerminalSessionClient != null)
|
|
mTermuxTerminalSessionClient.onStop();
|
|
|
|
if (mTermuxTerminalViewClient != null)
|
|
mTermuxTerminalViewClient.onStop();
|
|
|
|
removeTermuxActivityRootViewGlobalLayoutListener();
|
|
|
|
unregisterTermuxActivityBroadcastReceiever();
|
|
getDrawer().closeDrawers();
|
|
}
|
|
|
|
@Override
|
|
public void onDestroy() {
|
|
super.onDestroy();
|
|
|
|
Logger.logDebug(LOG_TAG, "onDestroy");
|
|
|
|
if (mIsInvalidState) return;
|
|
|
|
if (mTermuxService != null) {
|
|
// Do not leave service and session clients with references to activity.
|
|
mTermuxService.unsetTermuxTerminalSessionClient();
|
|
mTermuxService = null;
|
|
}
|
|
|
|
try {
|
|
unbindService(this);
|
|
} catch (Exception e) {
|
|
// ignore.
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
|
|
Logger.logVerbose(LOG_TAG, "onSaveInstanceState");
|
|
|
|
super.onSaveInstanceState(savedInstanceState);
|
|
saveTerminalToolbarTextInput(savedInstanceState);
|
|
savedInstanceState.putBoolean(ARG_ACTIVITY_RECREATED, true);
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
* 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) {
|
|
|
|
Logger.logDebug(LOG_TAG, "onServiceConnected");
|
|
|
|
mTermuxService = ((TermuxService.LocalBinder) service).service;
|
|
|
|
setTermuxSessionsListView();
|
|
|
|
final Intent intent = getIntent();
|
|
setIntent(null);
|
|
|
|
if (mTermuxService.isTermuxSessionsEmpty()) {
|
|
if (mIsVisible) {
|
|
TermuxInstaller.setupBootstrapIfNeeded(TermuxActivity.this, () -> {
|
|
if (mTermuxService == null) return; // Activity might have been destroyed.
|
|
try {
|
|
boolean launchFailsafe = false;
|
|
if (intent != null && intent.getExtras() != null) {
|
|
launchFailsafe = intent.getExtras().getBoolean(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false);
|
|
}
|
|
mTermuxTerminalSessionClient.addNewSession(launchFailsafe, null);
|
|
} catch (WindowManager.BadTokenException e) {
|
|
// Activity finished - ignore.
|
|
}
|
|
});
|
|
} else {
|
|
// The service connected while not in foreground - just bail out.
|
|
finishActivityIfNotFinishing();
|
|
}
|
|
} else {
|
|
// If termux was started from launcher "New session" shortcut and activity is recreated,
|
|
// then the original intent will be re-delivered, resulting in a new session being re-added
|
|
// each time.
|
|
if (!mIsActivityRecreated && intent != null && Intent.ACTION_RUN.equals(intent.getAction())) {
|
|
// Android 7.1 app shortcut from res/xml/shortcuts.xml.
|
|
boolean isFailSafe = intent.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false);
|
|
mTermuxTerminalSessionClient.addNewSession(isFailSafe, null);
|
|
} else {
|
|
mTermuxTerminalSessionClient.setCurrentSession(mTermuxTerminalSessionClient.getCurrentStoredSessionOrLast());
|
|
}
|
|
}
|
|
|
|
// Update the {@link TerminalSession} and {@link TerminalEmulator} clients.
|
|
mTermuxService.setTermuxTerminalSessionClient(mTermuxTerminalSessionClient);
|
|
}
|
|
|
|
@Override
|
|
public void onServiceDisconnected(ComponentName name) {
|
|
|
|
Logger.logDebug(LOG_TAG, "onServiceDisconnected");
|
|
|
|
// Respect being stopped from the {@link TermuxService} notification action.
|
|
finishActivityIfNotFinishing();
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void setActivityTheme() {
|
|
// Update NightMode.APP_NIGHT_MODE
|
|
TermuxThemeUtils.setAppNightMode(mProperties.getNightMode());
|
|
|
|
// Set activity night mode. If NightMode.SYSTEM is set, then android will automatically
|
|
// trigger recreation of activity when uiMode/dark mode configuration is changed so that
|
|
// day or night theme takes affect.
|
|
AppCompatActivityUtils.setNightMode(this, NightMode.getAppNightMode().getName(), true);
|
|
}
|
|
|
|
private void setMargins() {
|
|
RelativeLayout relativeLayout = findViewById(R.id.activity_termux_root_relative_layout);
|
|
int marginHorizontal = mProperties.getTerminalMarginHorizontal();
|
|
int marginVertical = mProperties.getTerminalMarginVertical();
|
|
ViewUtils.setLayoutMarginsInDp(relativeLayout, marginHorizontal, marginVertical, marginHorizontal, marginVertical);
|
|
}
|
|
|
|
|
|
|
|
public void addTermuxActivityRootViewGlobalLayoutListener() {
|
|
getTermuxActivityRootView().getViewTreeObserver().addOnGlobalLayoutListener(getTermuxActivityRootView());
|
|
}
|
|
|
|
public void removeTermuxActivityRootViewGlobalLayoutListener() {
|
|
if (getTermuxActivityRootView() != null)
|
|
getTermuxActivityRootView().getViewTreeObserver().removeOnGlobalLayoutListener(getTermuxActivityRootView());
|
|
}
|
|
|
|
|
|
|
|
private void setTermuxTerminalViewAndClients() {
|
|
// Set termux terminal view and session clients
|
|
mTermuxTerminalSessionClient = new TermuxTerminalSessionClient(this);
|
|
mTermuxTerminalViewClient = new TermuxTerminalViewClient(this, mTermuxTerminalSessionClient);
|
|
|
|
// Set termux terminal view
|
|
mTerminalView = findViewById(R.id.terminal_view);
|
|
mTerminalView.setTerminalViewClient(mTermuxTerminalViewClient);
|
|
|
|
if (mTermuxTerminalViewClient != null)
|
|
mTermuxTerminalViewClient.onCreate();
|
|
|
|
if (mTermuxTerminalSessionClient != null)
|
|
mTermuxTerminalSessionClient.onCreate();
|
|
}
|
|
|
|
private void setTermuxSessionsListView() {
|
|
ListView termuxSessionsListView = findViewById(R.id.terminal_sessions_list);
|
|
mTermuxSessionListViewController = new TermuxSessionsListViewController(this, mTermuxService.getTermuxSessions());
|
|
termuxSessionsListView.setAdapter(mTermuxSessionListViewController);
|
|
termuxSessionsListView.setOnItemClickListener(mTermuxSessionListViewController);
|
|
termuxSessionsListView.setOnItemLongClickListener(mTermuxSessionListViewController);
|
|
}
|
|
|
|
|
|
|
|
private void setTerminalToolbarView(Bundle savedInstanceState) {
|
|
final ViewPager terminalToolbarViewPager = getTerminalToolbarViewPager();
|
|
if (mPreferences.shouldShowTerminalToolbar()) terminalToolbarViewPager.setVisibility(View.VISIBLE);
|
|
|
|
ViewGroup.LayoutParams layoutParams = terminalToolbarViewPager.getLayoutParams();
|
|
mTerminalToolbarDefaultHeight = layoutParams.height;
|
|
|
|
setTerminalToolbarHeight();
|
|
|
|
String savedTextInput = null;
|
|
if (savedInstanceState != null)
|
|
savedTextInput = savedInstanceState.getString(ARG_TERMINAL_TOOLBAR_TEXT_INPUT);
|
|
|
|
terminalToolbarViewPager.setAdapter(new TerminalToolbarViewPager.PageAdapter(this, savedTextInput));
|
|
terminalToolbarViewPager.addOnPageChangeListener(new TerminalToolbarViewPager.OnPageChangeListener(this, terminalToolbarViewPager));
|
|
}
|
|
|
|
private void setTerminalToolbarHeight() {
|
|
final ViewPager terminalToolbarViewPager = getTerminalToolbarViewPager();
|
|
if (terminalToolbarViewPager == null) return;
|
|
|
|
ViewGroup.LayoutParams layoutParams = terminalToolbarViewPager.getLayoutParams();
|
|
layoutParams.height = (int) Math.round(mTerminalToolbarDefaultHeight *
|
|
(mProperties.getExtraKeysInfo() == null ? 0 : mProperties.getExtraKeysInfo().getMatrix().length) *
|
|
mProperties.getTerminalToolbarHeightScaleFactor());
|
|
terminalToolbarViewPager.setLayoutParams(layoutParams);
|
|
}
|
|
|
|
public void toggleTerminalToolbar() {
|
|
final ViewPager terminalToolbarViewPager = getTerminalToolbarViewPager();
|
|
if (terminalToolbarViewPager == null) return;
|
|
|
|
final boolean showNow = mPreferences.toogleShowTerminalToolbar();
|
|
Logger.showToast(this, (showNow ? getString(R.string.msg_enabling_terminal_toolbar) : getString(R.string.msg_disabling_terminal_toolbar)), true);
|
|
terminalToolbarViewPager.setVisibility(showNow ? View.VISIBLE : View.GONE);
|
|
if (showNow && isTerminalToolbarTextInputViewSelected()) {
|
|
// Focus the text input view if just revealed.
|
|
findViewById(R.id.terminal_toolbar_text_input).requestFocus();
|
|
}
|
|
}
|
|
|
|
private void saveTerminalToolbarTextInput(Bundle savedInstanceState) {
|
|
if (savedInstanceState == null) return;
|
|
|
|
final EditText textInputView = findViewById(R.id.terminal_toolbar_text_input);
|
|
if (textInputView != null) {
|
|
String textInput = textInputView.getText().toString();
|
|
if (!textInput.isEmpty()) savedInstanceState.putString(ARG_TERMINAL_TOOLBAR_TEXT_INPUT, textInput);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private void setSettingsButtonView() {
|
|
ImageButton settingsButton = findViewById(R.id.settings_button);
|
|
settingsButton.setOnClickListener(v -> {
|
|
ActivityUtils.startActivity(this, new Intent(this, SettingsActivity.class));
|
|
});
|
|
}
|
|
|
|
private void setNewSessionButtonView() {
|
|
View newSessionButton = findViewById(R.id.new_session_button);
|
|
newSessionButton.setOnClickListener(v -> mTermuxTerminalSessionClient.addNewSession(false, null));
|
|
newSessionButton.setOnLongClickListener(v -> {
|
|
TextInputDialogUtils.textInput(TermuxActivity.this, R.string.title_create_named_session, null,
|
|
R.string.action_create_named_session_confirm, text -> mTermuxTerminalSessionClient.addNewSession(false, text),
|
|
R.string.action_new_session_failsafe, text -> mTermuxTerminalSessionClient.addNewSession(true, text),
|
|
-1, null, null);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
private void setToggleKeyboardView() {
|
|
findViewById(R.id.toggle_keyboard_button).setOnClickListener(v -> {
|
|
mTermuxTerminalViewClient.onToggleSoftKeyboardRequest();
|
|
getDrawer().closeDrawers();
|
|
});
|
|
|
|
findViewById(R.id.toggle_keyboard_button).setOnLongClickListener(v -> {
|
|
//toggleTerminalToolbar();
|
|
return true;
|
|
});
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@SuppressLint("RtlHardcoded")
|
|
@Override
|
|
public void onBackPressed() {
|
|
if (getDrawer().isDrawerOpen(Gravity.LEFT)) {
|
|
getDrawer().closeDrawers();
|
|
} else {
|
|
finishActivityIfNotFinishing();
|
|
}
|
|
}
|
|
|
|
public void finishActivityIfNotFinishing() {
|
|
// prevent duplicate calls to finish() if called from multiple places
|
|
if (!TermuxActivity.this.isFinishing()) {
|
|
finish();
|
|
}
|
|
}
|
|
|
|
/** Show a toast and dismiss the last one if still visible. */
|
|
public void showToast(String text, boolean longDuration) {
|
|
if (text == null || text.isEmpty()) return;
|
|
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();
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
|
TerminalSession currentSession = getCurrentSession();
|
|
if (currentSession == null) return;
|
|
|
|
boolean addAutoFillMenu = false;
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
AutofillManager autofillManager = getSystemService(AutofillManager.class);
|
|
if (autofillManager != null && autofillManager.isEnabled()) {
|
|
addAutoFillMenu = true;
|
|
}
|
|
}
|
|
|
|
menu.add(Menu.NONE, CONTEXT_MENU_SELECT_URL_ID, Menu.NONE, R.string.action_select_url);
|
|
menu.add(Menu.NONE, CONTEXT_MENU_SHARE_TRANSCRIPT_ID, Menu.NONE, R.string.action_share_transcript);
|
|
if (addAutoFillMenu) menu.add(Menu.NONE, CONTEXT_MENU_AUTOFILL_ID, Menu.NONE, R.string.action_autofill_password);
|
|
menu.add(Menu.NONE, CONTEXT_MENU_RESET_TERMINAL_ID, Menu.NONE, R.string.action_reset_terminal);
|
|
menu.add(Menu.NONE, CONTEXT_MENU_KILL_PROCESS_ID, Menu.NONE, getResources().getString(R.string.action_kill_process, getCurrentSession().getPid())).setEnabled(currentSession.isRunning());
|
|
menu.add(Menu.NONE, CONTEXT_MENU_STYLING_ID, Menu.NONE, R.string.action_style_terminal);
|
|
menu.add(Menu.NONE, CONTEXT_MENU_TOGGLE_KEEP_SCREEN_ON, Menu.NONE, R.string.action_toggle_keep_screen_on).setCheckable(true).setChecked(mPreferences.shouldKeepScreenOn());
|
|
menu.add(Menu.NONE, CONTEXT_MENU_HELP_ID, Menu.NONE, R.string.action_open_help);
|
|
menu.add(Menu.NONE, CONTEXT_MENU_SETTINGS_ID, Menu.NONE, R.string.action_open_settings);
|
|
menu.add(Menu.NONE, CONTEXT_MENU_REPORT_ID, Menu.NONE, R.string.action_report_issue);
|
|
}
|
|
|
|
/** Hook system menu to show context menu instead. */
|
|
@Override
|
|
public boolean onCreateOptionsMenu(Menu menu) {
|
|
mTerminalView.showContextMenu();
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean onContextItemSelected(MenuItem item) {
|
|
TerminalSession session = getCurrentSession();
|
|
|
|
switch (item.getItemId()) {
|
|
case CONTEXT_MENU_SELECT_URL_ID:
|
|
mTermuxTerminalViewClient.showUrlSelection();
|
|
return true;
|
|
case CONTEXT_MENU_SHARE_TRANSCRIPT_ID:
|
|
mTermuxTerminalViewClient.shareSessionTranscript();
|
|
return true;
|
|
case CONTEXT_MENU_AUTOFILL_ID:
|
|
requestAutoFill();
|
|
return true;
|
|
case CONTEXT_MENU_RESET_TERMINAL_ID:
|
|
onResetTerminalSession(session);
|
|
return true;
|
|
case CONTEXT_MENU_KILL_PROCESS_ID:
|
|
showKillSessionDialog(session);
|
|
return true;
|
|
case CONTEXT_MENU_STYLING_ID:
|
|
showStylingDialog();
|
|
return true;
|
|
case CONTEXT_MENU_TOGGLE_KEEP_SCREEN_ON:
|
|
toggleKeepScreenOn();
|
|
return true;
|
|
case CONTEXT_MENU_HELP_ID:
|
|
ActivityUtils.startActivity(this, new Intent(this, HelpActivity.class));
|
|
return true;
|
|
case CONTEXT_MENU_SETTINGS_ID:
|
|
ActivityUtils.startActivity(this, new Intent(this, SettingsActivity.class));
|
|
return true;
|
|
case CONTEXT_MENU_REPORT_ID:
|
|
mTermuxTerminalViewClient.reportIssueFromTranscript();
|
|
return true;
|
|
default:
|
|
return super.onContextItemSelected(item);
|
|
}
|
|
}
|
|
|
|
private void showKillSessionDialog(TerminalSession session) {
|
|
if (session == null) return;
|
|
|
|
final AlertDialog.Builder b = new AlertDialog.Builder(this);
|
|
b.setIcon(android.R.drawable.ic_dialog_alert);
|
|
b.setMessage(R.string.title_confirm_kill_process);
|
|
b.setPositiveButton(android.R.string.yes, (dialog, id) -> {
|
|
dialog.dismiss();
|
|
session.finishIfRunning();
|
|
});
|
|
b.setNegativeButton(android.R.string.no, null);
|
|
b.show();
|
|
}
|
|
|
|
private void onResetTerminalSession(TerminalSession session) {
|
|
if (session != null) {
|
|
session.reset();
|
|
showToast(getResources().getString(R.string.msg_terminal_reset), true);
|
|
|
|
if (mTermuxTerminalSessionClient != null)
|
|
mTermuxTerminalSessionClient.onResetTerminalSession();
|
|
}
|
|
}
|
|
|
|
private void showStylingDialog() {
|
|
Intent stylingIntent = new Intent();
|
|
stylingIntent.setClassName(TermuxConstants.TERMUX_STYLING_PACKAGE_NAME, TermuxConstants.TERMUX_STYLING.TERMUX_STYLING_ACTIVITY_NAME);
|
|
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(getString(R.string.error_styling_not_installed))
|
|
.setPositiveButton(R.string.action_styling_install,
|
|
(dialog, which) -> ActivityUtils.startActivity(this, new Intent(Intent.ACTION_VIEW, Uri.parse(TermuxConstants.TERMUX_STYLING_FDROID_PACKAGE_URL))))
|
|
.setNegativeButton(android.R.string.cancel, null).show();
|
|
}
|
|
}
|
|
private void toggleKeepScreenOn() {
|
|
if (mTerminalView.getKeepScreenOn()) {
|
|
mTerminalView.setKeepScreenOn(false);
|
|
mPreferences.setKeepScreenOn(false);
|
|
} else {
|
|
mTerminalView.setKeepScreenOn(true);
|
|
mPreferences.setKeepScreenOn(true);
|
|
}
|
|
}
|
|
|
|
private void requestAutoFill() {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
AutofillManager autofillManager = getSystemService(AutofillManager.class);
|
|
if (autofillManager != null && autofillManager.isEnabled()) {
|
|
autofillManager.requestAutofill(mTerminalView);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* For processes to access primary external storage (/sdcard, /storage/emulated/0, ~/storage/shared),
|
|
* termux needs to be granted legacy WRITE_EXTERNAL_STORAGE or MANAGE_EXTERNAL_STORAGE permissions
|
|
* if targeting targetSdkVersion 30 (android 11) and running on sdk 30 (android 11) and higher.
|
|
*/
|
|
public void requestStoragePermission(boolean isPermissionCallback) {
|
|
new Thread() {
|
|
@Override
|
|
public void run() {
|
|
// Do not ask for permission again
|
|
int requestCode = isPermissionCallback ? -1 : PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION;
|
|
|
|
// If permission is granted, then also setup storage symlinks.
|
|
if(PermissionUtils.checkAndRequestLegacyOrManageExternalStoragePermission(
|
|
TermuxActivity.this, requestCode, !isPermissionCallback)) {
|
|
if (isPermissionCallback)
|
|
Logger.logInfoAndShowToast(TermuxActivity.this, LOG_TAG,
|
|
getString(com.termux.shared.R.string.msg_storage_permission_granted_on_request));
|
|
|
|
TermuxInstaller.setupStorageSymlinks(TermuxActivity.this);
|
|
} else {
|
|
if (isPermissionCallback)
|
|
Logger.logInfoAndShowToast(TermuxActivity.this, LOG_TAG,
|
|
getString(com.termux.shared.R.string.msg_storage_permission_not_granted_on_request));
|
|
}
|
|
}
|
|
}.start();
|
|
}
|
|
|
|
@Override
|
|
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
|
super.onActivityResult(requestCode, resultCode, data);
|
|
Logger.logVerbose(LOG_TAG, "onActivityResult: requestCode: " + requestCode + ", resultCode: " + resultCode + ", data: " + IntentUtils.getIntentString(data));
|
|
if (requestCode == PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION) {
|
|
requestStoragePermission(true);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
|
Logger.logVerbose(LOG_TAG, "onRequestPermissionsResult: requestCode: " + requestCode + ", permissions: " + Arrays.toString(permissions) + ", grantResults: " + Arrays.toString(grantResults));
|
|
if (requestCode == PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION) {
|
|
requestStoragePermission(true);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public int getNavBarHeight() {
|
|
return mNavBarHeight;
|
|
}
|
|
|
|
public TermuxActivityRootView getTermuxActivityRootView() {
|
|
return mTermuxActivityRootView;
|
|
}
|
|
|
|
public View getTermuxActivityBottomSpaceView() {
|
|
return mTermuxActivityBottomSpaceView;
|
|
}
|
|
|
|
public ExtraKeysView getExtraKeysView() {
|
|
return mExtraKeysView;
|
|
}
|
|
|
|
public void setExtraKeysView(ExtraKeysView extraKeysView) {
|
|
mExtraKeysView = extraKeysView;
|
|
}
|
|
|
|
public DrawerLayout getDrawer() {
|
|
return (DrawerLayout) findViewById(R.id.drawer_layout);
|
|
}
|
|
|
|
|
|
public ViewPager getTerminalToolbarViewPager() {
|
|
return (ViewPager) findViewById(R.id.terminal_toolbar_view_pager);
|
|
}
|
|
|
|
public boolean isTerminalViewSelected() {
|
|
return getTerminalToolbarViewPager().getCurrentItem() == 0;
|
|
}
|
|
|
|
public boolean isTerminalToolbarTextInputViewSelected() {
|
|
return getTerminalToolbarViewPager().getCurrentItem() == 1;
|
|
}
|
|
|
|
|
|
public void termuxSessionListNotifyUpdated() {
|
|
mTermuxSessionListViewController.notifyDataSetChanged();
|
|
}
|
|
|
|
public boolean isVisible() {
|
|
return mIsVisible;
|
|
}
|
|
|
|
public boolean isOnResumeAfterOnCreate() {
|
|
return isOnResumeAfterOnCreate;
|
|
}
|
|
|
|
public boolean isActivityRecreated() {
|
|
return mIsActivityRecreated;
|
|
}
|
|
|
|
|
|
|
|
public TermuxService getTermuxService() {
|
|
return mTermuxService;
|
|
}
|
|
|
|
public TerminalView getTerminalView() {
|
|
return mTerminalView;
|
|
}
|
|
|
|
public TermuxTerminalViewClient getTermuxTerminalViewClient() {
|
|
return mTermuxTerminalViewClient;
|
|
}
|
|
|
|
public TermuxTerminalSessionClient getTermuxTerminalSessionClient() {
|
|
return mTermuxTerminalSessionClient;
|
|
}
|
|
|
|
@Nullable
|
|
public TerminalSession getCurrentSession() {
|
|
if (mTerminalView != null)
|
|
return mTerminalView.getCurrentSession();
|
|
else
|
|
return null;
|
|
}
|
|
|
|
public TermuxAppSharedPreferences getPreferences() {
|
|
return mPreferences;
|
|
}
|
|
|
|
public TermuxAppSharedProperties getProperties() {
|
|
return mProperties;
|
|
}
|
|
|
|
|
|
|
|
|
|
public static void updateTermuxActivityStyling(Context context, boolean recreateActivity) {
|
|
// Make sure that terminal styling is always applied.
|
|
Intent stylingIntent = new Intent(TERMUX_ACTIVITY.ACTION_RELOAD_STYLE);
|
|
stylingIntent.putExtra(TERMUX_ACTIVITY.EXTRA_RECREATE_ACTIVITY, recreateActivity);
|
|
context.sendBroadcast(stylingIntent);
|
|
}
|
|
|
|
private void registerTermuxActivityBroadcastReceiver() {
|
|
IntentFilter intentFilter = new IntentFilter();
|
|
intentFilter.addAction(TERMUX_ACTIVITY.ACTION_REQUEST_PERMISSIONS);
|
|
intentFilter.addAction(TERMUX_ACTIVITY.ACTION_RELOAD_STYLE);
|
|
|
|
registerReceiver(mTermuxActivityBroadcastReceiver, intentFilter);
|
|
}
|
|
|
|
private void unregisterTermuxActivityBroadcastReceiever() {
|
|
unregisterReceiver(mTermuxActivityBroadcastReceiver);
|
|
}
|
|
|
|
private void fixTermuxActivityBroadcastReceieverIntent(Intent intent) {
|
|
if (intent == null) return;
|
|
|
|
String extraReloadStyle = intent.getStringExtra(TERMUX_ACTIVITY.EXTRA_RELOAD_STYLE);
|
|
if ("storage".equals(extraReloadStyle)) {
|
|
intent.removeExtra(TERMUX_ACTIVITY.EXTRA_RELOAD_STYLE);
|
|
intent.setAction(TERMUX_ACTIVITY.ACTION_REQUEST_PERMISSIONS);
|
|
}
|
|
}
|
|
|
|
class TermuxActivityBroadcastReceiver extends BroadcastReceiver {
|
|
@Override
|
|
public void onReceive(Context context, Intent intent) {
|
|
if (intent == null) return;
|
|
|
|
if (mIsVisible) {
|
|
fixTermuxActivityBroadcastReceieverIntent(intent);
|
|
|
|
switch (intent.getAction()) {
|
|
case TERMUX_ACTIVITY.ACTION_REQUEST_PERMISSIONS:
|
|
Logger.logDebug(LOG_TAG, "Received intent to request storage permissions");
|
|
requestStoragePermission(false);
|
|
return;
|
|
case TERMUX_ACTIVITY.ACTION_RELOAD_STYLE:
|
|
Logger.logDebug(LOG_TAG, "Received intent to reload styling");
|
|
reloadActivityStyling(intent.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_RECREATE_ACTIVITY, true));
|
|
return;
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void reloadActivityStyling(boolean recreateActivity) {
|
|
if (mProperties != null) {
|
|
mProperties.loadTermuxPropertiesFromDisk();
|
|
|
|
if (mExtraKeysView != null) {
|
|
mExtraKeysView.setButtonTextAllCaps(mProperties.shouldExtraKeysTextBeAllCaps());
|
|
mExtraKeysView.reload(mProperties.getExtraKeysInfo());
|
|
}
|
|
|
|
// Update NightMode.APP_NIGHT_MODE
|
|
TermuxThemeUtils.setAppNightMode(mProperties.getNightMode());
|
|
}
|
|
|
|
setMargins();
|
|
setTerminalToolbarHeight();
|
|
|
|
if (mTermuxTerminalSessionClient != null)
|
|
mTermuxTerminalSessionClient.onReload();
|
|
|
|
if (mTermuxTerminalViewClient != null)
|
|
mTermuxTerminalViewClient.onReload();
|
|
|
|
if (mTermuxService != null)
|
|
mTermuxService.setTerminalTranscriptRows();
|
|
|
|
// To change the activity and drawer theme, activity needs to be recreated.
|
|
// It will destroy the activity, including all stored variables and views, and onCreate()
|
|
// will be called again. Extra keys input text, terminal sessions and transcripts will be preserved.
|
|
if (recreateActivity) {
|
|
Logger.logDebug(LOG_TAG, "Recreating activity");
|
|
TermuxActivity.this.recreate();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public static void startTermuxActivity(@NonNull final Context context) {
|
|
ActivityUtils.startActivity(context, newInstance(context));
|
|
}
|
|
|
|
public static Intent newInstance(@NonNull final Context context) {
|
|
Intent intent = new Intent(context, TermuxActivity.class);
|
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
return intent;
|
|
}
|
|
|
|
}
|