diff --git a/package-lock.json b/package-lock.json
index c9e575333..3df5a6262 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4645,9 +4645,9 @@
}
},
"node_modules/cordova-android/node_modules/dedent": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz",
- "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==",
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz",
+ "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
diff --git a/package.json b/package.json
index 278957a56..7dbbe1723 100644
--- a/package.json
+++ b/package.json
@@ -37,8 +37,8 @@
"cordova-plugin-browser": {},
"cordova-plugin-sftp": {},
"cordova-plugin-system": {},
- "com.foxdebug.acode.rk.exec.terminal": {},
- "com.foxdebug.acode.rk.exec.proot": {}
+ "com.foxdebug.acode.rk.exec.proot": {},
+ "com.foxdebug.acode.rk.exec.terminal": {}
},
"platforms": [
"android"
@@ -129,4 +129,4 @@
"yargs": "^18.0.0"
},
"browserslist": "cover 100%,not android < 5"
-}
\ No newline at end of file
+}
diff --git a/src/components/terminal/terminalManager.js b/src/components/terminal/terminalManager.js
index 9c2f424a1..94c1eae94 100644
--- a/src/components/terminal/terminalManager.js
+++ b/src/components/terminal/terminalManager.js
@@ -588,10 +588,6 @@ class TerminalManager {
// Remove from map
this.terminals.delete(terminalId);
- if (this.getAllTerminals().size <= 0) {
- Executor.stopService();
- }
-
console.log(`Terminal ${terminalId} closed`);
} catch (error) {
console.error(`Error closing terminal ${terminalId}:`, error);
diff --git a/src/plugins/terminal/plugin.xml b/src/plugins/terminal/plugin.xml
index fbde6010f..27e78fc14 100644
--- a/src/plugins/terminal/plugin.xml
+++ b/src/plugins/terminal/plugin.xml
@@ -20,9 +20,9 @@
-
-
+
+
@@ -31,25 +31,32 @@
+ android:name="com.foxdebug.acode.rk.exec.terminal.AlpineDocumentProvider"
+ android:authorities="${applicationId}.documents"
+ android:exported="true"
+ android:grantUriPermissions="true"
+ android:icon="@mipmap/ic_launcher"
+ android:permission="android.permission.MANAGE_DOCUMENTS">
+
+
+
+
+
+
+
@@ -57,9 +64,5 @@
-
-
-
-
\ No newline at end of file
diff --git a/src/plugins/terminal/src/android/Executor.java b/src/plugins/terminal/src/android/Executor.java
index 3e0b4e83d..61ebcef03 100644
--- a/src/plugins/terminal/src/android/Executor.java
+++ b/src/plugins/terminal/src/android/Executor.java
@@ -3,250 +3,91 @@
import org.apache.cordova.*;
import org.json.*;
-import android.content.ComponentName;
+import org.apache.cordova.CordovaInterface;
+import org.apache.cordova.CordovaPlugin;
+import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.CallbackContext;
+import org.apache.cordova.PluginResult;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.*;
+import java.io.OutputStream;
+
+import java.util.Map;
+import java.util.HashMap;
+import java.util.TimeZone;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
import android.content.Context;
import android.content.Intent;
-import android.content.ServiceConnection;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.Message;
-import android.os.Messenger;
-import android.os.RemoteException;
-import android.util.Log;
-
-import java.util.UUID;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import android.Manifest;
-import android.content.pm.PackageManager;
import android.os.Build;
+import android.content.pm.PackageManager;
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.core.app.ActivityCompat;
-import androidx.core.content.ContextCompat;
-import android.app.Activity;
public class Executor extends CordovaPlugin {
- private Messenger serviceMessenger;
- private boolean isServiceBound;
- private boolean isServiceBinding; // Track if binding is in progress
+ private final Map processes = new ConcurrentHashMap<>();
+ private final Map processInputs = new ConcurrentHashMap<>();
+ private final Map processCallbacks = new ConcurrentHashMap<>();
+
private Context context;
- private Activity activity;
- private final Messenger handlerMessenger = new Messenger(new IncomingHandler());
- private CountDownLatch serviceConnectedLatch;
- private final java.util.Map callbackContextMap = new java.util.concurrent.ConcurrentHashMap<>();
-
- private static final int REQUEST_POST_NOTIFICATIONS = 1001;
-
- private void askNotificationPermission(Activity context) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- if (ContextCompat.checkSelfPermission(
- context, Manifest.permission.POST_NOTIFICATIONS) ==
- PackageManager.PERMISSION_GRANTED) {
- } else if (ActivityCompat.shouldShowRequestPermissionRationale(
- context, Manifest.permission.POST_NOTIFICATIONS)) {
- ActivityCompat.requestPermissions(
- context,
- new String[]{Manifest.permission.POST_NOTIFICATIONS},
- REQUEST_POST_NOTIFICATIONS
- );
- } else {
- ActivityCompat.requestPermissions(
- context,
- new String[]{Manifest.permission.POST_NOTIFICATIONS},
- REQUEST_POST_NOTIFICATIONS
- );
- }
- }
- }
- @Override
- public void initialize(CordovaInterface cordova, CordovaWebView webView) {
- super.initialize(cordova, webView);
- this.context = cordova.getContext();
- this.activity = cordova.getActivity();
- askNotificationPermission(activity);
-
- // Don't bind service immediately - wait until needed
- Log.d("Executor", "Plugin initialized - service will be started when needed");
- }
- /**
- * Ensure service is bound and ready for communication
- * Returns true if service is ready, false if binding failed
- */
- private boolean ensureServiceBound(CallbackContext callbackContext) {
- // If already bound, return immediately
- if (isServiceBound && serviceMessenger != null) {
- return true;
- }
+ @Override
+ public void initialize(CordovaInterface cordova, CordovaWebView webView) {
+ super.initialize(cordova, webView);
+ this.context = cordova.getContext();
+
+ }
- // If binding is already in progress, wait for it
- if (isServiceBinding) {
- try {
- if (serviceConnectedLatch != null &&
- serviceConnectedLatch.await(10, TimeUnit.SECONDS)) {
- return isServiceBound;
- } else {
- callbackContext.error("Service binding timeout");
- return false;
- }
- } catch (InterruptedException e) {
- callbackContext.error("Service binding interrupted: " + e.getMessage());
- return false;
+ private void checkStopService() {
+ boolean hasRunning = false;
+
+ for (Process p : processes.values()) {
+ if (p != null && p.isAlive()) {
+ hasRunning = true;
+ break;
}
}
- // Start binding process
- Log.d("Executor", "Starting service binding...");
- return bindServiceNow(callbackContext);
+ if (!hasRunning) {
+ // Stop foreground service
+ Intent serviceIntent = new Intent(context, KeepAliveService.class);
+ context.stopService(serviceIntent);
+ }
}
- /**
- * Immediately bind to service
- */
- private boolean bindServiceNow(CallbackContext callbackContext) {
- if (isServiceBinding) {
- return false; // Already binding
- }
+ private void startService(){
+ Intent serviceIntent = new Intent(this.context, KeepAliveService.class);
- isServiceBinding = true;
- serviceConnectedLatch = new CountDownLatch(1);
-
- Intent intent = new Intent(context, TerminalService.class);
-
- // Start the service first
- context.startService(intent);
-
- // Then bind to it
- boolean bindResult = context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
-
- if (!bindResult) {
- Log.e("Executor", "Failed to bind to service");
- isServiceBinding = false;
- callbackContext.error("Failed to bind to service");
- return false;
- }
-
- // Wait for connection
- try {
- if (serviceConnectedLatch.await(10, TimeUnit.SECONDS)) {
- Log.d("Executor", "Service bound successfully");
- return isServiceBound;
- } else {
- Log.e("Executor", "Service binding timeout");
- callbackContext.error("Service binding timeout");
- isServiceBinding = false;
- return false;
- }
- } catch (InterruptedException e) {
- Log.e("Executor", "Service binding interrupted: " + e.getMessage());
- callbackContext.error("Service binding interrupted: " + e.getMessage());
- isServiceBinding = false;
- return false;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ // Android 8.0+ needs startForegroundService()
+ this.context.startForegroundService(serviceIntent);
+ } else {
+ // Older versions use startService()
+ this.context.startService(serviceIntent);
}
}
- private final ServiceConnection serviceConnection = new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- Log.d("Executor", "Service connected");
- serviceMessenger = new Messenger(service);
- isServiceBound = true;
- isServiceBinding = false;
- if (serviceConnectedLatch != null) {
- serviceConnectedLatch.countDown();
- }
- }
+
- @Override
- public void onServiceDisconnected(ComponentName name) {
- Log.w("Executor", "Service disconnected");
- serviceMessenger = null;
- isServiceBound = false;
- isServiceBinding = false;
- serviceConnectedLatch = new CountDownLatch(1);
- }
- };
-
- private class IncomingHandler extends Handler {
- @Override
- public void handleMessage(Message msg) {
- Bundle bundle = msg.getData();
- String id = bundle.getString("id");
- String action = bundle.getString("action");
- String data = bundle.getString("data");
-
- if (action.equals("exec_result")) {
- CallbackContext callbackContext = getCallbackContext(id);
- if (callbackContext != null) {
- if (bundle.getBoolean("isSuccess", false)) {
- callbackContext.success(data);
- } else {
- callbackContext.error(data);
- }
- cleanupCallback(id);
- }
- } else {
- String pid = id;
- CallbackContext callbackContext = getCallbackContext(pid);
-
- if (callbackContext != null) {
- switch (action) {
- case "stdout":
- case "stderr":
- PluginResult result = new PluginResult(PluginResult.Status.OK, action + ":" + data);
- result.setKeepCallback(true);
- callbackContext.sendPluginResult(result);
- break;
- case "exit":
- cleanupCallback(pid);
- callbackContext.success("exit:" + data);
- break;
- case "isRunning":
- callbackContext.success(data);
- cleanupCallback(pid);
- break;
- }
- }
- }
- }
- }
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
- // For actions that don't need the service, handle them directly
- if (action.equals("loadLibrary")) {
- try {
- System.load(args.getString(0));
- callbackContext.success("Library loaded successfully.");
- } catch (Exception e) {
- callbackContext.error("Failed to load library: " + e.getMessage());
- }
- return true;
- }
+ switch (action) {
+ case "start":
- if (action.equals("stopService")) {
- stopServiceNow();
- callbackContext.success("Service stopped");
- return true;
- }
+ startService();
- // For all other actions, ensure service is bound first
- if (!ensureServiceBound(callbackContext)) {
- // Error already sent by ensureServiceBound
- return false;
- }
- switch (action) {
- case "start":
String cmdStart = args.getString(0);
String pid = UUID.randomUUID().toString();
- callbackContextMap.put(pid, callbackContext);
- startProcess(pid, cmdStart, args.getString(1));
+ startProcess(pid, cmdStart,args.getString(1), callbackContext);
return true;
case "write":
String pidWrite = args.getString(0);
@@ -258,14 +99,22 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo
stopProcess(pidStop, callbackContext);
return true;
case "exec":
- String execId = UUID.randomUUID().toString();
- callbackContextMap.put(execId, callbackContext);
- exec(execId, args.getString(0), args.getString(1));
+
+ startService();
+
+
+ exec(args.getString(0),args.getString(1), callbackContext);
return true;
case "isRunning":
- String pidCheck = args.getString(0);
- callbackContextMap.put(pidCheck, callbackContext);
- isProcessRunning(pidCheck);
+ isProcessRunning(args.getString(0), callbackContext);
+ return true;
+ case "loadLibrary":
+ try {
+ System.load(args.getString(0));
+ callbackContext.success("Library loaded successfully.");
+ } catch (Exception e) {
+ callbackContext.error("Failed to load library: " + e.getMessage());
+ }
return true;
default:
callbackContext.error("Unknown action: " + action);
@@ -273,127 +122,194 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo
}
}
- private void stopServiceNow() {
- if (isServiceBound) {
- try {
- context.unbindService(serviceConnection);
- Log.d("Executor", "Service unbound");
- } catch (IllegalArgumentException ignored) {
- // already unbound
- }
- isServiceBound = false;
- }
- isServiceBinding = false;
-
- Intent intent = new Intent(context, TerminalService.class);
- boolean stopped = context.stopService(intent);
- Log.d("Executor", "Service stop result: " + stopped);
-
- serviceMessenger = null;
- if (serviceConnectedLatch == null) {
- serviceConnectedLatch = new CountDownLatch(1);
- }
- }
+ private void exec(String cmd,String alpine, CallbackContext callbackContext) {
+ cordova.getThreadPool().execute(() -> {
+ try {
+ if (cmd != null && !cmd.isEmpty()) {
+ String xcmd = cmd;
+ if(alpine.equals("true")){
+ xcmd = "source $PREFIX/init-sandbox.sh "+cmd;
+ }
+
+ ProcessBuilder builder = new ProcessBuilder("sh", "-c", xcmd);
+
+ // Set environment variables
+ Map env = builder.environment();
+ env.put("PREFIX", context.getFilesDir().getAbsolutePath());
+ env.put("NATIVE_DIR", context.getApplicationInfo().nativeLibraryDir);
+ TimeZone tz = TimeZone.getDefault();
+ String timezoneId = tz.getID();
+ env.put("ANDROID_TZ", timezoneId);
+
+ try {
+ int target = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).applicationInfo.targetSdkVersion;
+ env.put("FDROID", String.valueOf(target <= 28));
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ }
- private void startProcess(String pid, String cmd, String alpine) {
- CallbackContext callbackContext = getCallbackContext(pid);
- if (callbackContext != null) {
- PluginResult result = new PluginResult(PluginResult.Status.OK, pid);
- result.setKeepCallback(true);
- callbackContext.sendPluginResult(result);
- }
- Message msg = Message.obtain(null, TerminalService.MSG_START_PROCESS);
- msg.replyTo = handlerMessenger;
- Bundle bundle = new Bundle();
- bundle.putString("id", pid);
- bundle.putString("cmd", cmd);
- bundle.putString("alpine", alpine);
- msg.setData(bundle);
- try {
- serviceMessenger.send(msg);
- } catch (RemoteException e) {
- CallbackContext errorContext = getCallbackContext(pid);
- if (errorContext != null) {
- errorContext.error("Failed to start process: " + e.getMessage());
- cleanupCallback(pid);
+ Process process = builder.start();
+
+ // Capture stdout
+ BufferedReader stdOutReader = new BufferedReader(
+ new InputStreamReader(process.getInputStream()));
+ StringBuilder stdOut = new StringBuilder();
+ String line;
+ while ((line = stdOutReader.readLine()) != null) {
+ stdOut.append(line).append("\n");
+ }
+
+ // Capture stderr
+ BufferedReader stdErrReader = new BufferedReader(
+ new InputStreamReader(process.getErrorStream()));
+ StringBuilder stdErr = new StringBuilder();
+ while ((line = stdErrReader.readLine()) != null) {
+ stdErr.append(line).append("\n");
+ }
+
+ int exitCode = process.waitFor();
+ if (exitCode == 0) {
+ callbackContext.success(stdOut.toString().trim());
+ } else {
+ String errorOutput = stdErr.toString().trim();
+ if (errorOutput.isEmpty()) {
+ errorOutput = "Command exited with code: " + exitCode;
+ }
+ callbackContext.error(errorOutput);
+ }
+ } else {
+ callbackContext.error("Expected one non-empty string argument.");
}
+ } catch (Exception e) {
+ e.printStackTrace();
+ callbackContext.error("Exception: " + e.getMessage());
}
+ });
}
- private void exec(String execId, String cmd, String alpine) {
- Message msg = Message.obtain(null, TerminalService.MSG_EXEC);
- msg.replyTo = handlerMessenger;
- Bundle bundle = new Bundle();
- bundle.putString("id", execId);
- bundle.putString("cmd", cmd);
- bundle.putString("alpine", alpine);
- msg.setData(bundle);
- try {
- serviceMessenger.send(msg);
- } catch (RemoteException e) {
- CallbackContext callbackContext = getCallbackContext(execId);
- if (callbackContext != null) {
- callbackContext.error("Failed to execute command: " + e.getMessage());
- cleanupCallback(execId);
+ private void startProcess(String pid, String cmd,String alpine, CallbackContext callbackContext) {
+ cordova.getThreadPool().execute(() -> {
+ try {
+ String xcmd = cmd;
+ if(alpine.equals("true")){
+ xcmd = "source $PREFIX/init-sandbox.sh "+cmd;
+ }
+ ProcessBuilder builder = new ProcessBuilder("sh", "-c", xcmd);
+
+ // Set environment variables
+ Map env = builder.environment();
+ env.put("PREFIX", context.getFilesDir().getAbsolutePath());
+ env.put("NATIVE_DIR", context.getApplicationInfo().nativeLibraryDir);
+ TimeZone tz = TimeZone.getDefault();
+ String timezoneId = tz.getID();
+ env.put("ANDROID_TZ", timezoneId);
+
+ try {
+ int target = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).applicationInfo.targetSdkVersion;
+ env.put("FDROID", String.valueOf(target <= 28));
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ }
+
+
+
+ Process process = builder.start();
+
+ processes.put(pid, process);
+ processInputs.put(pid, process.getOutputStream());
+ processCallbacks.put(pid, callbackContext);
+
+ PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, pid);
+ pluginResult.setKeepCallback(true);
+ callbackContext.sendPluginResult(pluginResult);
+
+ // stdout thread
+ new Thread(() -> streamOutput(process.getInputStream(), pid, "stdout")).start();
+ // stderr thread
+ new Thread(() -> streamOutput(process.getErrorStream(), pid, "stderr")).start();
+
+ int exitCode = process.waitFor();
+ sendPluginMessage(pid, "exit:" + exitCode);
+ cleanup(pid);
+ } catch (Exception e) {
+ callbackContext.error("Failed to start process: " + e.getMessage());
}
- }
+ });
}
private void writeToProcess(String pid, String input, CallbackContext callbackContext) {
- Message msg = Message.obtain(null, TerminalService.MSG_WRITE_TO_PROCESS);
- Bundle bundle = new Bundle();
- bundle.putString("id", pid);
- bundle.putString("input", input);
- msg.setData(bundle);
try {
- serviceMessenger.send(msg);
- callbackContext.success("Written to process");
- } catch (RemoteException e) {
+ OutputStream os = processInputs.get(pid);
+ if (os != null) {
+ os.write((input + "\n").getBytes());
+ os.flush();
+ callbackContext.success("Written to process");
+ } else {
+ callbackContext.error("Process not found or closed");
+ }
+ } catch (IOException e) {
callbackContext.error("Write error: " + e.getMessage());
}
}
private void stopProcess(String pid, CallbackContext callbackContext) {
- Message msg = Message.obtain(null, TerminalService.MSG_STOP_PROCESS);
- Bundle bundle = new Bundle();
- bundle.putString("id", pid);
- msg.setData(bundle);
- try {
- serviceMessenger.send(msg);
+ Process process = processes.get(pid);
+ if (process != null) {
+ process.destroy();
+ cleanup(pid);
+ // Check if we should stop the service
+ checkStopService();
callbackContext.success("Process terminated");
- } catch (RemoteException e) {
- callbackContext.error("Stop error: " + e.getMessage());
+ } else {
+ callbackContext.error("No such process");
}
}
- private void isProcessRunning(String pid) {
- Message msg = Message.obtain(null, TerminalService.MSG_IS_RUNNING);
- msg.replyTo = handlerMessenger;
- Bundle bundle = new Bundle();
- bundle.putString("id", pid);
- msg.setData(bundle);
- try {
- serviceMessenger.send(msg);
- } catch (RemoteException e) {
- CallbackContext callbackContext = getCallbackContext(pid);
- if (callbackContext != null) {
- callbackContext.error("Check running error: " + e.getMessage());
- cleanupCallback(pid);
+ private void isProcessRunning(String pid, CallbackContext callbackContext) {
+ Process process = processes.get(pid);
+
+ if (process != null) {
+ if (process.isAlive()) {
+ callbackContext.success("running");
+ } else {
+ cleanup(pid);
+ callbackContext.success("exited");
}
+ } else {
+ callbackContext.success("not_found");
}
}
- private CallbackContext getCallbackContext(String id) {
- return callbackContextMap.get(id);
+ private void streamOutput(InputStream inputStream, String pid, String streamType) {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ sendPluginMessage(pid, streamType + ":" + line);
+ }
+ } catch (IOException ignored) {
+ }
}
- private void cleanupCallback(String id) {
- callbackContextMap.remove(id);
+ private void sendPluginMessage(String pid, String message) {
+ CallbackContext ctx = processCallbacks.get(pid);
+ if (ctx != null) {
+ PluginResult result = new PluginResult(PluginResult.Status.OK, message);
+ result.setKeepCallback(true);
+ ctx.sendPluginResult(result);
+ }
}
@Override
public void onDestroy() {
super.onDestroy();
+ //Intent serviceIntent = new Intent(context, KeepAliveService.class);
+ //context.stopService(serviceIntent);
+ }
+
+ private void cleanup(String pid) {
+ processes.remove(pid);
+ processInputs.remove(pid);
+ processCallbacks.remove(pid);
}
}
\ No newline at end of file
diff --git a/src/plugins/terminal/src/android/KeepAliveService.java b/src/plugins/terminal/src/android/KeepAliveService.java
new file mode 100644
index 000000000..ca35546b2
--- /dev/null
+++ b/src/plugins/terminal/src/android/KeepAliveService.java
@@ -0,0 +1,117 @@
+package com.foxdebug.acode.rk.exec.terminal;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.PowerManager;
+
+public class KeepAliveService extends Service {
+
+ private static final String CHANNEL_ID = "keepalive_channel";
+ private static final int NOTIFICATION_ID = 101;
+
+ private PowerManager.WakeLock wakeLock;
+
+ public static final String ACTION_ACQUIRE = "ACQUIRE_WAKELOCK";
+ public static final String ACTION_EXIT = "EXIT_SERVICE";
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ createNotificationChannel();
+ startForeground(NOTIFICATION_ID, buildNotification());
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+
+ if (intent != null && intent.getAction() != null) {
+ switch (intent.getAction()) {
+
+ case ACTION_ACQUIRE:
+ acquireWakeLock();
+ break;
+
+ case ACTION_EXIT:
+ stopSelf();
+ break;
+ }
+ }
+
+ // Update notification (in case state changed)
+ startForeground(NOTIFICATION_ID, buildNotification());
+ return START_STICKY;
+ }
+
+ private void acquireWakeLock() {
+ if (wakeLock == null) {
+ PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
+ wakeLock = pm.newWakeLock(
+ PowerManager.PARTIAL_WAKE_LOCK,
+ "KeepAliveService:WakeLock");
+ wakeLock.setReferenceCounted(false);
+ }
+
+ if (!wakeLock.isHeld()) {
+ wakeLock.acquire();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (wakeLock != null && wakeLock.isHeld()) {
+ wakeLock.release();
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ private Notification buildNotification() {
+
+ // Intent: Acquire WakeLock
+ Intent acquireIntent = new Intent(this, KeepAliveService.class);
+ acquireIntent.setAction(ACTION_ACQUIRE);
+
+ PendingIntent acquirePending = PendingIntent.getService(
+ this, 0, acquireIntent, PendingIntent.FLAG_IMMUTABLE);
+
+ // Intent: Exit Service
+ Intent exitIntent = new Intent(this, KeepAliveService.class);
+ exitIntent.setAction(ACTION_EXIT);
+
+ PendingIntent exitPending = PendingIntent.getService(
+ this, 1, exitIntent, PendingIntent.FLAG_IMMUTABLE);
+
+ Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID)
+ .setContentTitle("KeepAlive Service")
+ .setContentText("Running in foreground")
+ .setSmallIcon(android.R.drawable.ic_lock_idle_alarm)
+ .addAction(0, "Acquire WakeLock", acquirePending)
+ .addAction(0, "Exit", exitPending)
+ .setOngoing(true);
+
+ return builder.build();
+ }
+
+ private void createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+
+ NotificationChannel channel = new NotificationChannel(
+ CHANNEL_ID,
+ "KeepAlive",
+ NotificationManager.IMPORTANCE_LOW);
+
+ NotificationManager manager = getSystemService(NotificationManager.class);
+ manager.createNotificationChannel(channel);
+ }
+ }
+}
diff --git a/src/plugins/terminal/src/android/TerminalService.java b/src/plugins/terminal/src/android/TerminalService.java
deleted file mode 100644
index f8e26c67f..000000000
--- a/src/plugins/terminal/src/android/TerminalService.java
+++ /dev/null
@@ -1,413 +0,0 @@
-package com.foxdebug.acode.rk.exec.terminal;
-
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.Message;
-import android.os.Messenger;
-import android.os.PowerManager;
-import android.os.RemoteException;
-import androidx.core.app.NotificationCompat;
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.Executors;
-import java.lang.reflect.Field;
-
-import java.util.TimeZone;
-import java.util.Map;
-import java.util.HashMap;
-
-
-
-public class TerminalService extends Service {
-
- public static final int MSG_START_PROCESS = 1;
- public static final int MSG_WRITE_TO_PROCESS = 2;
- public static final int MSG_STOP_PROCESS = 3;
- public static final int MSG_IS_RUNNING = 4;
- public static final int MSG_EXEC = 5;
-
- public static final String CHANNEL_ID = "terminal_exec_channel";
-
- public static final String ACTION_EXIT_SERVICE = "com.foxdebug.acode.ACTION_EXIT_SERVICE";
- public static final String ACTION_TOGGLE_WAKE_LOCK = "com.foxdebug.acode.ACTION_TOGGLE_WAKE_LOCK";
-
- private final Map processes = new ConcurrentHashMap<>();
- private final Map processInputs = new ConcurrentHashMap<>();
- private final Map clientMessengers = new ConcurrentHashMap<>();
- private final java.util.concurrent.ExecutorService threadPool = Executors.newCachedThreadPool();
-
- private final Messenger serviceMessenger = new Messenger(new ServiceHandler());
-
- private PowerManager.WakeLock wakeLock;
- private boolean isWakeLockHeld = false;
-
- @Override
- public IBinder onBind(Intent intent) {
- return serviceMessenger.getBinder();
- }
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- if (intent != null) {
- String action = intent.getAction();
- if (ACTION_EXIT_SERVICE.equals(action)) {
- stopForeground(true);
- stopSelf();
- return START_NOT_STICKY;
- } else if (ACTION_TOGGLE_WAKE_LOCK.equals(action)) {
- toggleWakeLock();
- }
- }
- return START_STICKY;
- }
-
- private class ServiceHandler extends Handler {
- @Override
- public void handleMessage(Message msg) {
- Bundle bundle = msg.getData();
- String id = bundle.getString("id");
- Messenger clientMessenger = msg.replyTo;
-
- switch (msg.what) {
- case MSG_START_PROCESS:
- String cmd = bundle.getString("cmd");
- String alpine = bundle.getString("alpine");
- clientMessengers.put(id, clientMessenger);
- startProcess(id, cmd, alpine);
- break;
- case MSG_WRITE_TO_PROCESS:
- String input = bundle.getString("input");
- writeToProcess(id, input);
- break;
- case MSG_STOP_PROCESS:
- stopProcess(id);
- break;
- case MSG_IS_RUNNING:
- isProcessRunning(id, clientMessenger);
- break;
- case MSG_EXEC:
- String execCmd = bundle.getString("cmd");
- String execAlpine = bundle.getString("alpine");
- clientMessengers.put(id, clientMessenger);
- exec(id, execCmd, execAlpine);
- break;
- }
- }
- }
-
- private void toggleWakeLock() {
- if (isWakeLockHeld) {
- releaseWakeLock();
- } else {
- acquireWakeLock();
- }
- updateNotification();
- }
-
- private void acquireWakeLock() {
- if (wakeLock == null) {
- PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
- wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "AcodeTerminal:WakeLock");
- }
-
- if (!isWakeLockHeld) {
- wakeLock.acquire();
- isWakeLockHeld = true;
- }
- }
-
- private void releaseWakeLock() {
- if (wakeLock != null && isWakeLockHeld) {
- wakeLock.release();
- isWakeLockHeld = false;
- }
- }
-
- private void startProcess(String pid, String cmd, String alpine) {
- threadPool.execute(() -> {
- try {
- String xcmd = alpine.equals("true") ? "source $PREFIX/init-sandbox.sh " + cmd : cmd;
- ProcessBuilder builder = new ProcessBuilder("sh", "-c", xcmd);
-
- Map env = builder.environment();
- env.put("PREFIX", getFilesDir().getAbsolutePath());
- env.put("NATIVE_DIR", getApplicationInfo().nativeLibraryDir);
- TimeZone tz = TimeZone.getDefault();
- String timezoneId = tz.getID();
- env.put("ANDROID_TZ", timezoneId);
-
- try {
- int target = getPackageManager().getPackageInfo(getPackageName(), 0).applicationInfo.targetSdkVersion;
- env.put("FDROID", String.valueOf(target <= 28));
- } catch (Exception e) {
- e.printStackTrace();
- }
-
- Process process = builder.start();
- processes.put(pid, process);
- processInputs.put(pid, process.getOutputStream());
- threadPool.execute(() -> streamOutput(process.getInputStream(), pid, "stdout"));
- threadPool.execute(() -> streamOutput(process.getErrorStream(), pid, "stderr"));
- threadPool.execute(() -> {
- try {
- int exitCode = process.waitFor();
- sendMessageToClient(pid, "exit", String.valueOf(exitCode));
- cleanup(pid);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- });
- } catch (IOException e) {
- e.printStackTrace();
- sendMessageToClient(pid, "stderr", "Failed to start process: " + e.getMessage());
- sendMessageToClient(pid, "exit", "1");
- cleanup(pid);
- }
- });
- }
-
- private void exec(String execId, String cmd, String alpine) {
- threadPool.execute(() -> {
- try {
- String xcmd = alpine.equals("true") ? "source $PREFIX/init-sandbox.sh " + cmd : cmd;
- ProcessBuilder builder = new ProcessBuilder("sh", "-c", xcmd);
- Map env = builder.environment();
- env.put("PREFIX", getFilesDir().getAbsolutePath());
- env.put("NATIVE_DIR", getApplicationInfo().nativeLibraryDir);
- TimeZone tz = TimeZone.getDefault();
- String timezoneId = tz.getID();
- env.put("ANDROID_TZ", timezoneId);
-
- try {
- int target = getPackageManager().getPackageInfo(getPackageName(), 0).applicationInfo.targetSdkVersion;
- env.put("FDROID", String.valueOf(target <= 28));
- } catch (Exception e) {
- e.printStackTrace();
- }
-
- Process process = builder.start();
- BufferedReader stdOutReader = new BufferedReader(
- new InputStreamReader(process.getInputStream()));
- StringBuilder stdOut = new StringBuilder();
- String line;
- while ((line = stdOutReader.readLine()) != null) {
- stdOut.append(line).append("\n");
- }
-
- BufferedReader stdErrReader = new BufferedReader(
- new InputStreamReader(process.getErrorStream()));
- StringBuilder stdErr = new StringBuilder();
- while ((line = stdErrReader.readLine()) != null) {
- stdErr.append(line).append("\n");
- }
-
- int exitCode = process.waitFor();
-
- if (exitCode == 0) {
- sendExecResultToClient(execId, true, stdOut.toString().trim());
- } else {
- String errorOutput = stdErr.toString().trim();
- if (errorOutput.isEmpty()) {
- errorOutput = "Command exited with code: " + exitCode;
- }
- sendExecResultToClient(execId, false, errorOutput);
- }
-
- cleanup(execId);
- } catch (Exception e) {
- sendExecResultToClient(execId, false, "Exception: " + e.getMessage());
- cleanup(execId);
- }
- });
- }
-
- private void streamOutput(InputStream inputStream, String pid, String streamType) {
- try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
- String line;
- while ((line = reader.readLine()) != null) {
- sendMessageToClient(pid, streamType, line);
- }
- } catch (IOException ignored) {
- }
- }
-
- private void sendMessageToClient(String id, String action, String data) {
- Messenger clientMessenger = clientMessengers.get(id);
- if (clientMessenger != null) {
- try {
- Message msg = Message.obtain();
- Bundle bundle = new Bundle();
- bundle.putString("id", id);
- bundle.putString("action", action);
- bundle.putString("data", data);
- msg.setData(bundle);
- clientMessenger.send(msg);
- } catch (RemoteException e) {
- cleanup(id);
- }
- }
- }
-
- private void sendExecResultToClient(String id, boolean isSuccess, String data) {
- Messenger clientMessenger = clientMessengers.get(id);
- if (clientMessenger != null) {
- try {
- Message msg = Message.obtain();
- Bundle bundle = new Bundle();
- bundle.putString("id", id);
- bundle.putString("action", "exec_result");
- bundle.putString("data", data);
- bundle.putBoolean("isSuccess", isSuccess);
- msg.setData(bundle);
- clientMessenger.send(msg);
- } catch (RemoteException e) {
- cleanup(id);
- }
- }
- }
-
- private void writeToProcess(String pid, String input) {
- try {
- OutputStream os = processInputs.get(pid);
- if (os != null) {
- os.write((input + "\n").getBytes());
- os.flush();
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
-
- private long getPid(Process process) {
- try {
- Field f = process.getClass().getDeclaredField("pid");
- f.setAccessible(true);
- return f.getLong(process);
- } catch (Exception e) {
- return -1;
- }
-}
-
-
- private void stopProcess(String pid) {
- Process process = processes.get(pid);
- if (process != null) {
- try {
- Runtime.getRuntime().exec("kill -9 -" + getPid(process));
- } catch (Exception ignored) {}
- process.destroy();
- cleanup(pid);
- }
- }
-
- private void isProcessRunning(String pid, Messenger clientMessenger) {
- Process process = processes.get(pid);
- String status = process != null && isProcessAlive(process) ? "running" : "not_found";
- sendMessageToClient(pid, "isRunning", status);
- }
-
- private boolean isProcessAlive(Process process) {
- try {
- process.exitValue();
- return false;
- } catch(IllegalThreadStateException e) {
- return true;
- }
- }
-
- private void cleanup(String id) {
- processes.remove(id);
- processInputs.remove(id);
- clientMessengers.remove(id);
- }
-
- @Override
- public void onCreate() {
- super.onCreate();
- createNotificationChannel();
- updateNotification();
- }
-
- private void createNotificationChannel() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- NotificationChannel serviceChannel = new NotificationChannel(
- CHANNEL_ID,
- "Terminal Executor Channel",
- NotificationManager.IMPORTANCE_LOW
- );
- NotificationManager manager = getSystemService(NotificationManager.class);
- if (manager != null) {
- manager.createNotificationChannel(serviceChannel);
- }
- }
- }
-
- private void updateNotification() {
- Intent exitIntent = new Intent(this, TerminalService.class);
- exitIntent.setAction(ACTION_EXIT_SERVICE);
- PendingIntent exitPendingIntent = PendingIntent.getService(this, 0, exitIntent,
- PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
-
- Intent wakeLockIntent = new Intent(this, TerminalService.class);
- wakeLockIntent.setAction(ACTION_TOGGLE_WAKE_LOCK);
- PendingIntent wakeLockPendingIntent = PendingIntent.getService(this, 1, wakeLockIntent,
- PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
-
- String contentText = "Executor service" + (isWakeLockHeld ? " (wakelock held)" : "");
- String wakeLockButtonText = isWakeLockHeld ? "Release Wake Lock" : "Acquire Wake Lock";
-
- int notificationIcon = resolveDrawableId("ic_notification", "ic_launcher_foreground", "ic_launcher");
-
- Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
- .setContentTitle("Acode Service")
- .setContentText(contentText)
- .setSmallIcon(notificationIcon)
- .setOngoing(true)
- .addAction(notificationIcon, wakeLockButtonText, wakeLockPendingIntent)
- .addAction(notificationIcon, "Exit", exitPendingIntent)
- .build();
-
- startForeground(1, notification);
- }
-
- @Override
- public void onDestroy() {
- stopForeground(true);
- super.onDestroy();
- releaseWakeLock();
-
- for (Process process : processes.values()) {
- try {
- Runtime.getRuntime().exec("kill -9 -" + getPid(process));
- } catch (Exception ignored) {}
- process.destroyForcibly();
- }
-
- processes.clear();
- processInputs.clear();
- clientMessengers.clear();
- threadPool.shutdown();
- }
-
- private int resolveDrawableId(String... names) {
- for (String name : names) {
- int id = getResources().getIdentifier(name, "drawable", getPackageName());
- if (id != 0) return id;
- }
- return android.R.drawable.sym_def_app_icon;
- }
-}
diff --git a/src/plugins/terminal/www/Executor.js b/src/plugins/terminal/www/Executor.js
index 78103fafc..845e018f6 100644
--- a/src/plugins/terminal/www/Executor.js
+++ b/src/plugins/terminal/www/Executor.js
@@ -30,33 +30,22 @@ const Executor = {
*/
- start(command, onData) {
- this.start(command, onData, false);
+ start(command,onData){
+ this.start(command,onData,true)
},
start(command, onData, alpine) {
- console.log("start: " + command);
-
return new Promise((resolve, reject) => {
- let first = true;
- exec(async (message) => {
- console.log(message);
- if (first) {
- first = false;
- await new Promise(resolve => setTimeout(resolve, 100));
+ exec(
+ (message) => {
+ // Stream stdout, stderr, or exit notifications
+ if (message.startsWith("stdout:")) return onData("stdout", message.slice(7));
+ if (message.startsWith("stderr:")) return onData("stderr", message.slice(7));
+ if (message.startsWith("exit:")) return onData("exit", message.slice(5));
+
// First message is always the process UUID
resolve(message);
- } else {
- const match = message.match(/^([^:]+):(.*)$/);
- if (match) {
- const prefix = match[1]; // e.g. "stdout"
- const message = match[2].trim(); // output
- onData(prefix, message);
- } else {
- onData("unknown", message);
- }
- }
- },
+ },
reject,
"Executor",
"start",
@@ -73,10 +62,9 @@ const Executor = {
* @returns {Promise} Resolves once the input is written.
*
* @example
- * Executor.write(uuid, 'ls /sdcard');
+ * Executor.write(uuid, 'ls /data');
*/
write(uuid, input) {
- console.log("write: " + input + " to " + uuid);
return new Promise((resolve, reject) => {
exec(resolve, reject, "Executor", "write", [uuid, input]);
});
@@ -114,12 +102,6 @@ const Executor = {
});
},
- stopService() {
- return new Promise((resolve, reject) => {
- exec(resolve, reject, "Executor", "stopService", []);
- });
- },
-
/**
* Executes a shell command once and waits for it to finish.
* Unlike {@link Executor.start}, this does not stream output.
@@ -133,8 +115,8 @@ const Executor = {
* .then(console.log)
* .catch(console.error);
*/
- execute(command) {
- this.execute(command, false);
+ execute(command){
+ this.execute(command,false)
}
,
execute(command, alpine) {
@@ -143,7 +125,7 @@ const Executor = {
});
},
- loadLibrary(path) {
+ loadLibrary(path){
return new Promise((resolve, reject) => {
exec(resolve, reject, "Executor", "loadLibrary", [path]);
});