mirror of
https://github.com/fankes/termux-app.git
synced 2025-09-06 10:45:23 +08:00
Refactor TermuxService
- Dedicated functions have been created for various actions and commands. - The `startForeground()` call will be made both on `onCreate()` and `onStartCommand(). - The `stopForeground()` call will be made before every `onSelf()` call. - The references to `TermuxActivity` will now be removed in `onUnbind()` as well if activity `onDestroy()` failed to do so. - Appropriate log entries are added to help debug issues.
This commit is contained in:
@@ -63,12 +63,15 @@ public final class TermuxService extends Service {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The terminal sessions which this service manages.
|
* The terminal sessions which this service manages.
|
||||||
* <p/>
|
* Note that this list is observed by {@link TermuxActivity#mTermuxSessionListViewController},
|
||||||
* Note that this list is observed by {@link TermuxActivity#mTermuxSessionListViewController}, so any changes must be made on the UI
|
* so any changes must be made on the UI thread and followed by a call to
|
||||||
* thread and followed by a call to {@link ArrayAdapter#notifyDataSetChanged()} }.
|
* {@link ArrayAdapter#notifyDataSetChanged()} }.
|
||||||
*/
|
*/
|
||||||
final List<TerminalSession> mTerminalSessions = new ArrayList<>();
|
final List<TerminalSession> mTerminalSessions = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The background jobs which this service manages.
|
||||||
|
*/
|
||||||
final List<BackgroundJob> mBackgroundTasks = new ArrayList<>();
|
final List<BackgroundJob> mBackgroundTasks = new ArrayList<>();
|
||||||
|
|
||||||
/** The full implementation of the {@link TerminalSessionClient} interface to be used by {@link TerminalSession}
|
/** The full implementation of the {@link TerminalSessionClient} interface to be used by {@link TerminalSession}
|
||||||
@@ -86,89 +89,49 @@ public final class TermuxService extends Service {
|
|||||||
private PowerManager.WakeLock mWakeLock;
|
private PowerManager.WakeLock mWakeLock;
|
||||||
private WifiManager.WifiLock mWifiLock;
|
private WifiManager.WifiLock mWifiLock;
|
||||||
|
|
||||||
/** If the user has executed the {@link TermuxConstants.TERMUX_APP.TERMUX_SERVICE#ACTION_STOP_SERVICE} intent. */
|
/** If the user has executed the {@link TERMUX_SERVICE#ACTION_STOP_SERVICE} intent. */
|
||||||
boolean mWantsToStop = false;
|
boolean mWantsToStop = false;
|
||||||
|
|
||||||
private static final String LOG_TAG = "TermuxService";
|
private static final String LOG_TAG = "TermuxService";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
Logger.logVerbose(LOG_TAG, "onCreate");
|
||||||
|
runStartForeground();
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("Wakelock")
|
@SuppressLint("Wakelock")
|
||||||
@Override
|
@Override
|
||||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
|
Logger.logDebug(LOG_TAG, "onStartCommand");
|
||||||
|
|
||||||
|
// Run again in case service is already started and onCreate() is not called
|
||||||
|
runStartForeground();
|
||||||
|
|
||||||
String action = intent.getAction();
|
String action = intent.getAction();
|
||||||
if (TERMUX_SERVICE.ACTION_STOP_SERVICE.equals(action)) {
|
|
||||||
mWantsToStop = true;
|
|
||||||
for (int i = 0; i < mTerminalSessions.size(); i++)
|
|
||||||
mTerminalSessions.get(i).finishIfRunning();
|
|
||||||
stopSelf();
|
|
||||||
} else if (TERMUX_SERVICE.ACTION_WAKE_LOCK.equals(action)) {
|
|
||||||
if (mWakeLock == null) {
|
|
||||||
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
|
|
||||||
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TermuxConstants.TERMUX_APP_NAME.toLowerCase() + ":service-wakelock");
|
|
||||||
mWakeLock.acquire();
|
|
||||||
|
|
||||||
// http://tools.android.com/tech-docs/lint-in-studio-2-3#TOC-WifiManager-Leak
|
if (action != null) {
|
||||||
WifiManager wm = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
switch (action) {
|
||||||
mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, TermuxConstants.TERMUX_APP_NAME.toLowerCase());
|
case TERMUX_SERVICE.ACTION_STOP_SERVICE:
|
||||||
mWifiLock.acquire();
|
Logger.logDebug(LOG_TAG, "ACTION_STOP_SERVICE intent received");
|
||||||
|
actionStopService();
|
||||||
String packageName = getPackageName();
|
break;
|
||||||
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
|
case TERMUX_SERVICE.ACTION_WAKE_LOCK:
|
||||||
Intent whitelist = new Intent();
|
Logger.logDebug(LOG_TAG, "ACTION_WAKE_LOCK intent received");
|
||||||
whitelist.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
actionAcquireWakeLock();
|
||||||
whitelist.setData(Uri.parse("package:" + packageName));
|
break;
|
||||||
whitelist.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
case TERMUX_SERVICE.ACTION_WAKE_UNLOCK:
|
||||||
|
Logger.logDebug(LOG_TAG, "ACTION_WAKE_UNLOCK intent received");
|
||||||
try {
|
actionReleaseWakeLock(true);
|
||||||
startActivity(whitelist);
|
break;
|
||||||
} catch (ActivityNotFoundException e) {
|
case TERMUX_SERVICE.ACTION_SERVICE_EXECUTE:
|
||||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to call ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS", e);
|
Logger.logDebug(LOG_TAG, "ACTION_SERVICE_EXECUTE intent received");
|
||||||
}
|
actionServiceExecute(intent);
|
||||||
}
|
break;
|
||||||
|
default:
|
||||||
updateNotification();
|
Logger.logError(LOG_TAG, "Invalid action: \"" + action + "\"");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} else if (TERMUX_SERVICE.ACTION_WAKE_UNLOCK.equals(action)) {
|
|
||||||
if (mWakeLock != null) {
|
|
||||||
mWakeLock.release();
|
|
||||||
mWakeLock = null;
|
|
||||||
|
|
||||||
mWifiLock.release();
|
|
||||||
mWifiLock = null;
|
|
||||||
|
|
||||||
updateNotification();
|
|
||||||
}
|
|
||||||
} else if (TERMUX_SERVICE.ACTION_SERVICE_EXECUTE.equals(action)) {
|
|
||||||
Uri executableUri = intent.getData();
|
|
||||||
String executablePath = (executableUri == null ? null : executableUri.getPath());
|
|
||||||
|
|
||||||
String[] arguments = (executableUri == null ? null : intent.getStringArrayExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS));
|
|
||||||
String cwd = intent.getStringExtra(TERMUX_SERVICE.EXTRA_WORKDIR);
|
|
||||||
|
|
||||||
if (intent.getBooleanExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, false)) {
|
|
||||||
BackgroundJob task = new BackgroundJob(cwd, executablePath, arguments, this, intent.getParcelableExtra("pendingIntent"));
|
|
||||||
mBackgroundTasks.add(task);
|
|
||||||
updateNotification();
|
|
||||||
} else {
|
|
||||||
boolean failsafe = intent.getBooleanExtra(TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION, false);
|
|
||||||
TerminalSession newSession = createTermSession(executablePath, arguments, cwd, failsafe);
|
|
||||||
|
|
||||||
// Transform executable path to session name, e.g. "/bin/do-something.sh" => "do something.sh".
|
|
||||||
if (executablePath != null) {
|
|
||||||
int lastSlash = executablePath.lastIndexOf('/');
|
|
||||||
String name = (lastSlash == -1) ? executablePath : executablePath.substring(lastSlash + 1);
|
|
||||||
name = name.replace('-', ' ');
|
|
||||||
newSession.mSessionName = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make the newly created session the current one to be displayed:
|
|
||||||
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(this);
|
|
||||||
preferences.setCurrentSession(newSession.mHandle);
|
|
||||||
|
|
||||||
// Launch the main Termux app, which will now show the current session:
|
|
||||||
startActivity(new Intent(this, TermuxActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
|
|
||||||
}
|
|
||||||
} else if (action != null) {
|
|
||||||
Logger.logError(LOG_TAG, "Unknown TermuxService action: '" + action + "'");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this service really do get killed, there is no point restarting it automatically - let the user do on next
|
// If this service really do get killed, there is no point restarting it automatically - let the user do on next
|
||||||
@@ -176,27 +139,310 @@ public final class TermuxService extends Service {
|
|||||||
return Service.START_NOT_STICKY;
|
return Service.START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
Logger.logVerbose(LOG_TAG, "onDestroy");
|
||||||
|
File termuxTmpDir = TermuxConstants.TERMUX_TMP_DIR;
|
||||||
|
|
||||||
|
if (termuxTmpDir.exists()) {
|
||||||
|
try {
|
||||||
|
TermuxInstaller.deleteFolder(termuxTmpDir.getCanonicalFile());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.logStackTraceWithMessage(LOG_TAG, "Error while removing file at " + termuxTmpDir.getAbsolutePath(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
termuxTmpDir.mkdirs();
|
||||||
|
}
|
||||||
|
|
||||||
|
actionReleaseWakeLock(false);
|
||||||
|
finishAllTerminalSessions();
|
||||||
|
runStopForeground();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IBinder onBind(Intent intent) {
|
public IBinder onBind(Intent intent) {
|
||||||
|
Logger.logVerbose(LOG_TAG, "onBind");
|
||||||
return mBinder;
|
return mBinder;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public boolean onUnbind(Intent intent) {
|
||||||
|
Logger.logVerbose(LOG_TAG, "onUnbind");
|
||||||
|
|
||||||
|
// Since we cannot rely on {@link TermuxActivity.onDestroy()} to always complete,
|
||||||
|
// we unset clients here as well if it failed, so that we do not leave service and session
|
||||||
|
// clients with references to the activity.
|
||||||
|
if(mTermuxSessionClient != null)
|
||||||
|
unsetTermuxSessionClient();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Make service run in foreground mode. */
|
||||||
|
private void runStartForeground() {
|
||||||
setupNotificationChannel();
|
setupNotificationChannel();
|
||||||
startForeground(NOTIFICATION_ID, buildNotification());
|
startForeground(NOTIFICATION_ID, buildNotification());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Update the shown foreground service notification after making any changes that affect it. */
|
/** Make service leave foreground mode. */
|
||||||
void updateNotification() {
|
private void runStopForeground() {
|
||||||
if (mWakeLock == null && mTerminalSessions.isEmpty() && mBackgroundTasks.isEmpty()) {
|
stopForeground(true);
|
||||||
// Exit if we are updating after the user disabled all locks with no sessions or tasks running.
|
}
|
||||||
stopSelf();
|
|
||||||
|
/** Request to stop service. */
|
||||||
|
private void requestStopService() {
|
||||||
|
Logger.logDebug(LOG_TAG, "Requesting to stop service");
|
||||||
|
runStopForeground();
|
||||||
|
stopSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** Process action to stop service. */
|
||||||
|
private void actionStopService() {
|
||||||
|
mWantsToStop = true;
|
||||||
|
finishAllTerminalSessions();
|
||||||
|
requestStopService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Process action to acquire Power and Wi-Fi WakeLocks. */
|
||||||
|
@SuppressLint({"WakelockTimeout", "BatteryLife"})
|
||||||
|
private void actionAcquireWakeLock() {
|
||||||
|
if (mWakeLock == null) {
|
||||||
|
Logger.logDebug(LOG_TAG, "Acquiring WakeLocks");
|
||||||
|
|
||||||
|
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
|
||||||
|
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TermuxConstants.TERMUX_APP_NAME.toLowerCase() + ":service-wakelock");
|
||||||
|
mWakeLock.acquire();
|
||||||
|
|
||||||
|
// http://tools.android.com/tech-docs/lint-in-studio-2-3#TOC-WifiManager-Leak
|
||||||
|
WifiManager wm = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
||||||
|
mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, TermuxConstants.TERMUX_APP_NAME.toLowerCase());
|
||||||
|
mWifiLock.acquire();
|
||||||
|
|
||||||
|
String packageName = getPackageName();
|
||||||
|
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
|
||||||
|
Intent whitelist = new Intent();
|
||||||
|
whitelist.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||||
|
whitelist.setData(Uri.parse("package:" + packageName));
|
||||||
|
whitelist.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
|
||||||
|
try {
|
||||||
|
startActivity(whitelist);
|
||||||
|
} catch (ActivityNotFoundException e) {
|
||||||
|
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to call ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNotification();
|
||||||
|
|
||||||
|
Logger.logDebug(LOG_TAG, "WakeLocks acquired successfully");
|
||||||
} else {
|
} else {
|
||||||
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, buildNotification());
|
Logger.logDebug(LOG_TAG, "Ignoring acquiring WakeLocks since they are already held");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Process action to release Power and Wi-Fi WakeLocks. */
|
||||||
|
private void actionReleaseWakeLock(boolean updateNotification) {
|
||||||
|
if (mWakeLock == null && mWifiLock == null){
|
||||||
|
Logger.logDebug(LOG_TAG, "Ignoring releasing WakeLocks since none are already held");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.logDebug(LOG_TAG, "Releasing WakeLocks");
|
||||||
|
|
||||||
|
if (mWakeLock != null) {
|
||||||
|
mWakeLock.release();
|
||||||
|
mWakeLock = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mWifiLock != null) {
|
||||||
|
mWifiLock.release();
|
||||||
|
mWifiLock = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(updateNotification)
|
||||||
|
updateNotification();
|
||||||
|
|
||||||
|
Logger.logDebug(LOG_TAG, "WakeLocks released successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Process action to execute a shell command in a foreground session or in background. */
|
||||||
|
private void actionServiceExecute(Intent intent) {
|
||||||
|
Uri executableUri = intent.getData();
|
||||||
|
String executablePath = (executableUri == null ? null : executableUri.getPath());
|
||||||
|
|
||||||
|
String[] arguments = (executableUri == null ? null : intent.getStringArrayExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS));
|
||||||
|
String cwd = intent.getStringExtra(TERMUX_SERVICE.EXTRA_WORKDIR);
|
||||||
|
|
||||||
|
PendingIntent pendingIntent = intent.getParcelableExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT);
|
||||||
|
|
||||||
|
if (intent.getBooleanExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, false)) {
|
||||||
|
executeBackgroundCommand(executablePath, arguments, cwd, pendingIntent);
|
||||||
|
} else {
|
||||||
|
executeForegroundCommand(intent, executablePath, arguments, cwd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Execute a shell command in background with {@link BackgroundJob}. */
|
||||||
|
private void executeBackgroundCommand(String executablePath, String[] arguments, String cwd, PendingIntent pendingIntent) {
|
||||||
|
Logger.logDebug(LOG_TAG, "Starting background command");
|
||||||
|
|
||||||
|
BackgroundJob task = new BackgroundJob(cwd, executablePath, arguments, this, pendingIntent);
|
||||||
|
mBackgroundTasks.add(task);
|
||||||
|
updateNotification();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Callback received when a {@link BackgroundJob} finishes. */
|
||||||
|
public void onBackgroundJobExited(final BackgroundJob task) {
|
||||||
|
mHandler.post(() -> {
|
||||||
|
mBackgroundTasks.remove(task);
|
||||||
|
updateNotification();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Execute a shell command in a foreground terminal session. */
|
||||||
|
private void executeForegroundCommand(Intent intent, String executablePath, String[] arguments, String cwd) {
|
||||||
|
Logger.logDebug(LOG_TAG, "Starting foreground command");
|
||||||
|
|
||||||
|
boolean failsafe = intent.getBooleanExtra(TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION, false);
|
||||||
|
TerminalSession newSession = createTerminalSession(executablePath, arguments, cwd, failsafe);
|
||||||
|
|
||||||
|
// Transform executable path to session name, e.g. "/bin/do-something.sh" => "do something.sh".
|
||||||
|
if (executablePath != null) {
|
||||||
|
int lastSlash = executablePath.lastIndexOf('/');
|
||||||
|
String name = (lastSlash == -1) ? executablePath : executablePath.substring(lastSlash + 1);
|
||||||
|
name = name.replace('-', ' ');
|
||||||
|
newSession.mSessionName = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the newly created session the current one to be displayed:
|
||||||
|
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(this);
|
||||||
|
preferences.setCurrentSession(newSession.mHandle);
|
||||||
|
|
||||||
|
// Launch the main Termux app, which will now show the current session:
|
||||||
|
startActivity(new Intent(this, TermuxActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** Create a terminal session. */
|
||||||
|
public TerminalSession createTerminalSession(String executablePath, String[] arguments, String cwd, boolean failSafe) {
|
||||||
|
TermuxConstants.TERMUX_HOME_DIR.mkdirs();
|
||||||
|
|
||||||
|
if (cwd == null || cwd.isEmpty()) cwd = TermuxConstants.TERMUX_HOME_DIR_PATH;
|
||||||
|
|
||||||
|
String[] env = BackgroundJob.buildEnvironment(failSafe, cwd);
|
||||||
|
boolean isLoginShell = false;
|
||||||
|
|
||||||
|
if (executablePath == null) {
|
||||||
|
if (!failSafe) {
|
||||||
|
for (String shellBinary : new String[]{"login", "bash", "zsh"}) {
|
||||||
|
File shellFile = new File(TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH, shellBinary);
|
||||||
|
if (shellFile.canExecute()) {
|
||||||
|
executablePath = shellFile.getAbsolutePath();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (executablePath == null) {
|
||||||
|
// Fall back to system shell as last resort:
|
||||||
|
executablePath = "/system/bin/sh";
|
||||||
|
}
|
||||||
|
isLoginShell = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] processArgs = BackgroundJob.setupProcessArgs(executablePath, arguments);
|
||||||
|
executablePath = processArgs[0];
|
||||||
|
int lastSlashIndex = executablePath.lastIndexOf('/');
|
||||||
|
String processName = (isLoginShell ? "-" : "") +
|
||||||
|
(lastSlashIndex == -1 ? executablePath : executablePath.substring(lastSlashIndex + 1));
|
||||||
|
|
||||||
|
String[] args = new String[processArgs.length];
|
||||||
|
args[0] = processName;
|
||||||
|
if (processArgs.length > 1) System.arraycopy(processArgs, 1, args, 1, processArgs.length - 1);
|
||||||
|
|
||||||
|
TerminalSession session = new TerminalSession(executablePath, cwd, args, env, getTermuxSessionClient());
|
||||||
|
mTerminalSessions.add(session);
|
||||||
|
updateNotification();
|
||||||
|
|
||||||
|
// Make sure that terminal styling is always applied.
|
||||||
|
Intent stylingIntent = new Intent(TERMUX_ACTIVITY.ACTION_RELOAD_STYLE);
|
||||||
|
stylingIntent.putExtra(TERMUX_ACTIVITY.EXTRA_RELOAD_STYLE, "styling");
|
||||||
|
sendBroadcast(stylingIntent);
|
||||||
|
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove a terminal session. */
|
||||||
|
public int removeTerminalSession(TerminalSession sessionToRemove) {
|
||||||
|
int indexOfRemoved = mTerminalSessions.indexOf(sessionToRemove);
|
||||||
|
mTerminalSessions.remove(indexOfRemoved);
|
||||||
|
|
||||||
|
if (mTerminalSessions.isEmpty() && mWakeLock == null) {
|
||||||
|
// Finish if there are no sessions left and the wake lock is not held, otherwise keep the service alive if
|
||||||
|
// holding wake lock since there may be daemon processes (e.g. sshd) running.
|
||||||
|
requestStopService();
|
||||||
|
} else {
|
||||||
|
updateNotification();
|
||||||
|
}
|
||||||
|
return indexOfRemoved;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Finish all terminal sessions by sending SIGKILL to their shells. */
|
||||||
|
private void finishAllTerminalSessions() {
|
||||||
|
for (int i = 0; i < mTerminalSessions.size(); i++)
|
||||||
|
mTerminalSessions.get(i).finishIfRunning();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** If {@link TermuxActivity} has not bound to the {@link TermuxService} yet or is destroyed, then
|
||||||
|
* interface functions requiring the activity should not be available to the terminal sessions,
|
||||||
|
* so we just return the {@link #mTermuxSessionClientBase}. Once {@link TermuxActivity} bind
|
||||||
|
* callback is received, it should call {@link #setTermuxSessionClient} to set the
|
||||||
|
* {@link TermuxService#mTermuxSessionClient} so that further terminal sessions are directly
|
||||||
|
* passed the {@link TermuxSessionClient} object which fully implements the
|
||||||
|
* {@link TerminalSessionClient} interface.
|
||||||
|
*
|
||||||
|
* @return Returns the {@link TermuxSessionClient} if {@link TermuxActivity} has bound with
|
||||||
|
* {@link TermuxService}, otherwise {@link TermuxSessionClientBase}.
|
||||||
|
*/
|
||||||
|
public TermuxSessionClientBase getTermuxSessionClient() {
|
||||||
|
if (mTermuxSessionClient != null)
|
||||||
|
return mTermuxSessionClient;
|
||||||
|
else
|
||||||
|
return mTermuxSessionClientBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** This should be called when {@link TermuxActivity#onServiceConnected} is called to set the
|
||||||
|
* {@link TermuxService#mTermuxSessionClient} variable and update the {@link TerminalSession}
|
||||||
|
* and {@link TerminalEmulator} clients in case they were passed {@link TermuxSessionClientBase}
|
||||||
|
* earlier.
|
||||||
|
*
|
||||||
|
* @param termuxSessionClient The {@link TermuxSessionClient} object that fully
|
||||||
|
* implements the {@link TerminalSessionClient} interface.
|
||||||
|
*/
|
||||||
|
public void setTermuxSessionClient(TermuxSessionClient termuxSessionClient) {
|
||||||
|
mTermuxSessionClient = termuxSessionClient;
|
||||||
|
|
||||||
|
for (int i = 0; i < mTerminalSessions.size(); i++)
|
||||||
|
mTerminalSessions.get(i).updateTerminalSessionClient(mTermuxSessionClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** This should be called when {@link TermuxActivity} has been destroyed and in {@link #onUnbind(Intent)}
|
||||||
|
* so that the {@link TermuxService} and {@link TerminalSession} and {@link TerminalEmulator}
|
||||||
|
* clients do not hold an activity references.
|
||||||
|
*/
|
||||||
|
public void unsetTermuxSessionClient() {
|
||||||
|
for (int i = 0; i < mTerminalSessions.size(); i++)
|
||||||
|
mTerminalSessions.get(i).updateTerminalSessionClient(mTermuxSessionClientBase);
|
||||||
|
|
||||||
|
mTermuxSessionClient = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private Notification buildNotification() {
|
private Notification buildNotification() {
|
||||||
Intent notifyIntent = new Intent(this, TermuxActivity.class);
|
Intent notifyIntent = new Intent(this, TermuxActivity.class);
|
||||||
// PendingIntent#getActivity(): "Note that the activity will be started outside of the context of an existing
|
// PendingIntent#getActivity(): "Note that the activity will be started outside of the context of an existing
|
||||||
@@ -250,145 +496,6 @@ public final class TermuxService extends Service {
|
|||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
File termuxTmpDir = TermuxConstants.TERMUX_TMP_DIR;
|
|
||||||
|
|
||||||
if (termuxTmpDir.exists()) {
|
|
||||||
try {
|
|
||||||
TermuxInstaller.deleteFolder(termuxTmpDir.getCanonicalFile());
|
|
||||||
} catch (Exception e) {
|
|
||||||
Logger.logStackTraceWithMessage(LOG_TAG, "Error while removing file at " + termuxTmpDir.getAbsolutePath(), e);
|
|
||||||
}
|
|
||||||
|
|
||||||
termuxTmpDir.mkdirs();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mWakeLock != null) mWakeLock.release();
|
|
||||||
if (mWifiLock != null) mWifiLock.release();
|
|
||||||
|
|
||||||
stopForeground(true);
|
|
||||||
|
|
||||||
for (int i = 0; i < mTerminalSessions.size(); i++)
|
|
||||||
mTerminalSessions.get(i).finishIfRunning();
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<TerminalSession> getSessions() {
|
|
||||||
return mTerminalSessions;
|
|
||||||
}
|
|
||||||
|
|
||||||
public TerminalSession createTermSession(String executablePath, String[] arguments, String cwd, boolean failSafe) {
|
|
||||||
TermuxConstants.TERMUX_HOME_DIR.mkdirs();
|
|
||||||
|
|
||||||
if (cwd == null || cwd.isEmpty()) cwd = TermuxConstants.TERMUX_HOME_DIR_PATH;
|
|
||||||
|
|
||||||
String[] env = BackgroundJob.buildEnvironment(failSafe, cwd);
|
|
||||||
boolean isLoginShell = false;
|
|
||||||
|
|
||||||
if (executablePath == null) {
|
|
||||||
if (!failSafe) {
|
|
||||||
for (String shellBinary : new String[]{"login", "bash", "zsh"}) {
|
|
||||||
File shellFile = new File(TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH, shellBinary);
|
|
||||||
if (shellFile.canExecute()) {
|
|
||||||
executablePath = shellFile.getAbsolutePath();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (executablePath == null) {
|
|
||||||
// Fall back to system shell as last resort:
|
|
||||||
executablePath = "/system/bin/sh";
|
|
||||||
}
|
|
||||||
isLoginShell = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
String[] processArgs = BackgroundJob.setupProcessArgs(executablePath, arguments);
|
|
||||||
executablePath = processArgs[0];
|
|
||||||
int lastSlashIndex = executablePath.lastIndexOf('/');
|
|
||||||
String processName = (isLoginShell ? "-" : "") +
|
|
||||||
(lastSlashIndex == -1 ? executablePath : executablePath.substring(lastSlashIndex + 1));
|
|
||||||
|
|
||||||
String[] args = new String[processArgs.length];
|
|
||||||
args[0] = processName;
|
|
||||||
if (processArgs.length > 1) System.arraycopy(processArgs, 1, args, 1, processArgs.length - 1);
|
|
||||||
|
|
||||||
TerminalSession session = new TerminalSession(executablePath, cwd, args, env, getTermuxSessionClient());
|
|
||||||
mTerminalSessions.add(session);
|
|
||||||
updateNotification();
|
|
||||||
|
|
||||||
// Make sure that terminal styling is always applied.
|
|
||||||
Intent stylingIntent = new Intent(TERMUX_ACTIVITY.ACTION_RELOAD_STYLE);
|
|
||||||
stylingIntent.putExtra(TERMUX_ACTIVITY.EXTRA_RELOAD_STYLE, "styling");
|
|
||||||
sendBroadcast(stylingIntent);
|
|
||||||
|
|
||||||
return session;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int removeTermSession(TerminalSession sessionToRemove) {
|
|
||||||
int indexOfRemoved = mTerminalSessions.indexOf(sessionToRemove);
|
|
||||||
mTerminalSessions.remove(indexOfRemoved);
|
|
||||||
if (mTerminalSessions.isEmpty() && mWakeLock == null) {
|
|
||||||
// Finish if there are no sessions left and the wake lock is not held, otherwise keep the service alive if
|
|
||||||
// holding wake lock since there may be daemon processes (e.g. sshd) running.
|
|
||||||
stopSelf();
|
|
||||||
} else {
|
|
||||||
updateNotification();
|
|
||||||
}
|
|
||||||
return indexOfRemoved;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** If {@link TermuxActivity} has not bound to the {@link TermuxService} yet or is destroyed, then
|
|
||||||
* interface functions requiring the activity should not be available to the terminal sessions,
|
|
||||||
* so we just return the {@link #mTermuxSessionClientBase}. Once {@link TermuxActivity} bind
|
|
||||||
* callback is received, it should call {@link #setTermuxSessionClient} to set the
|
|
||||||
* {@link TermuxService#mTermuxSessionClient} so that further terminal sessions are directly
|
|
||||||
* passed the {@link TermuxSessionClient} object which fully implements the
|
|
||||||
* {@link TerminalSessionClient} interface.
|
|
||||||
*
|
|
||||||
* @return Returns the {@link TermuxSessionClient} if {@link TermuxActivity} has bound with
|
|
||||||
* {@link TermuxService}, otherwise {@link TermuxSessionClientBase}.
|
|
||||||
*/
|
|
||||||
public TermuxSessionClientBase getTermuxSessionClient() {
|
|
||||||
if (mTermuxSessionClient != null)
|
|
||||||
return mTermuxSessionClient;
|
|
||||||
else
|
|
||||||
return mTermuxSessionClientBase;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** This should be called when {@link TermuxActivity#onServiceConnected} is called to set the
|
|
||||||
* {@link TermuxService#mTermuxSessionClient} variable and update the {@link TerminalSession}
|
|
||||||
* and {@link TerminalEmulator} clients in case they were passed {@link TermuxSessionClientBase}
|
|
||||||
* earlier.
|
|
||||||
*
|
|
||||||
* @param termuxSessionClient The {@link TermuxSessionClient} object that fully
|
|
||||||
* implements the {@link TerminalSessionClient} interface.
|
|
||||||
*/
|
|
||||||
public void setTermuxSessionClient(TermuxSessionClient termuxSessionClient) {
|
|
||||||
mTermuxSessionClient = termuxSessionClient;
|
|
||||||
|
|
||||||
for (int i = 0; i < mTerminalSessions.size(); i++)
|
|
||||||
mTerminalSessions.get(i).updateTerminalSessionClient(mTermuxSessionClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** This should be called when {@link TermuxActivity} has been destroyed so that the
|
|
||||||
* {@link TermuxService} and {@link TerminalSession} and {@link TerminalEmulator} clients do not
|
|
||||||
* hold an activity references.
|
|
||||||
*/
|
|
||||||
public void unsetTermuxSessionClient() {
|
|
||||||
mTermuxSessionClient = null;
|
|
||||||
|
|
||||||
for (int i = 0; i < mTerminalSessions.size(); i++)
|
|
||||||
mTerminalSessions.get(i).updateTerminalSessionClient(mTermuxSessionClientBase);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onBackgroundJobExited(final BackgroundJob task) {
|
|
||||||
mHandler.post(() -> {
|
|
||||||
mBackgroundTasks.remove(task);
|
|
||||||
updateNotification();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setupNotificationChannel() {
|
private void setupNotificationChannel() {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
|
||||||
|
|
||||||
@@ -402,7 +509,24 @@ public final class TermuxService extends Service {
|
|||||||
manager.createNotificationChannel(channel);
|
manager.createNotificationChannel(channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Update the shown foreground service notification after making any changes that affect it. */
|
||||||
|
void updateNotification() {
|
||||||
|
if (mWakeLock == null && mTerminalSessions.isEmpty() && mBackgroundTasks.isEmpty()) {
|
||||||
|
// Exit if we are updating after the user disabled all locks with no sessions or tasks running.
|
||||||
|
requestStopService();
|
||||||
|
} else {
|
||||||
|
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, buildNotification());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public boolean wantsToStop() {
|
public boolean wantsToStop() {
|
||||||
return mWantsToStop;
|
return mWantsToStop;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<TerminalSession> getSessions() {
|
||||||
|
return mTerminalSessions;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -153,9 +153,10 @@ public class TermuxSessionClient extends TermuxSessionClientBase {
|
|||||||
final int indexOfSession = mActivity.getTermuxService().getSessions().indexOf(session);
|
final int indexOfSession = mActivity.getTermuxService().getSessions().indexOf(session);
|
||||||
mActivity.showToast(toToastTitle(session), false);
|
mActivity.showToast(toToastTitle(session), false);
|
||||||
mActivity.terminalSessionListNotifyUpdated();
|
mActivity.terminalSessionListNotifyUpdated();
|
||||||
final ListView lv = mActivity.findViewById(R.id.terminal_sessions_list);
|
|
||||||
lv.setItemChecked(indexOfSession, true);
|
final ListView termuxSessionsListView = mActivity.findViewById(R.id.terminal_sessions_list);
|
||||||
lv.smoothScrollToPosition(indexOfSession);
|
termuxSessionsListView.setItemChecked(indexOfSession, true);
|
||||||
|
termuxSessionsListView.smoothScrollToPosition(indexOfSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void switchToSession(boolean forward) {
|
public void switchToSession(boolean forward) {
|
||||||
@@ -195,7 +196,7 @@ public class TermuxSessionClient extends TermuxSessionClientBase {
|
|||||||
workingDirectory = currentSession.getCwd();
|
workingDirectory = currentSession.getCwd();
|
||||||
}
|
}
|
||||||
|
|
||||||
TerminalSession newSession = mActivity.getTermuxService().createTermSession(null, null, workingDirectory, failSafe);
|
TerminalSession newSession = mActivity.getTermuxService().createTerminalSession(null, null, workingDirectory, failSafe);
|
||||||
if (sessionName != null) {
|
if (sessionName != null) {
|
||||||
newSession.mSessionName = sessionName;
|
newSession.mSessionName = sessionName;
|
||||||
}
|
}
|
||||||
@@ -248,7 +249,7 @@ public class TermuxSessionClient extends TermuxSessionClientBase {
|
|||||||
// Return pressed with finished session - remove it.
|
// Return pressed with finished session - remove it.
|
||||||
TermuxService service = mActivity.getTermuxService();
|
TermuxService service = mActivity.getTermuxService();
|
||||||
|
|
||||||
int index = service.removeTermSession(finishedSession);
|
int index = service.removeTerminalSession(finishedSession);
|
||||||
mActivity.terminalSessionListNotifyUpdated();
|
mActivity.terminalSessionListNotifyUpdated();
|
||||||
if (mActivity.getTermuxService().getSessions().isEmpty()) {
|
if (mActivity.getTermuxService().getSessions().isEmpty()) {
|
||||||
// There are no sessions to show, so finish the activity.
|
// There are no sessions to show, so finish the activity.
|
||||||
|
Reference in New Issue
Block a user