Skip to content

Commit bf8b41f

Browse files
feat: exit + wakelock
1 parent 61408d3 commit bf8b41f

File tree

4 files changed

+148
-28
lines changed

4 files changed

+148
-28
lines changed

package-lock.json

Lines changed: 6 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"cordova-plugin-buildinfo": {},
3838
"cordova-plugin-browser": {},
3939
"cordova-plugin-system": {},
40+
"com.foxdebug.acode.rk.exec.proot": {},
4041
"com.foxdebug.acode.rk.exec.terminal": {}
4142
},
4243
"platforms": [
@@ -62,6 +63,7 @@
6263
"@types/url-parse": "^1.4.11",
6364
"autoprefixer": "^10.4.21",
6465
"babel-loader": "^10.0.0",
66+
"com.foxdebug.acode.rk.exec.proot": "file:src/plugins/proot",
6567
"com.foxdebug.acode.rk.exec.terminal": "file:src/plugins/terminal",
6668
"cordova-android": "^14.0.1",
6769
"cordova-clipboard": "^1.3.0",

src/plugins/terminal/src/android/Executor.java

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,26 +19,62 @@
1919
import java.util.concurrent.CountDownLatch;
2020
import java.util.concurrent.TimeUnit;
2121
//import com.foxdebug.acode.rk.exec.terminal.TerminalService;
22+
import android.Manifest;
23+
import android.content.pm.PackageManager;
24+
import android.os.Build;
25+
import android.os.Bundle;
26+
27+
import androidx.annotation.NonNull;
28+
import androidx.appcompat.app.AppCompatActivity;
29+
import androidx.core.app.ActivityCompat;
30+
import androidx.core.content.ContextCompat;
31+
import android.app.Activity;
2232

2333
public class Executor extends CordovaPlugin {
2434

2535
private Messenger serviceMessenger;
2636
private boolean isServiceBound;
2737
private Context context;
38+
private Activity activity;
2839
private final Messenger handlerMessenger = new Messenger(new IncomingHandler());
2940
private CountDownLatch serviceConnectedLatch = new CountDownLatch(1);
3041
private final java.util.Map<String, CallbackContext> callbackContextMap = new java.util.concurrent.ConcurrentHashMap<>();
3142

43+
private static final int REQUEST_POST_NOTIFICATIONS = 1001;
44+
private void askNotificationPermission(Activity context) {
45+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
46+
if (ContextCompat.checkSelfPermission(
47+
context, Manifest.permission.POST_NOTIFICATIONS) ==
48+
PackageManager.PERMISSION_GRANTED) {
49+
} else if (ActivityCompat.shouldShowRequestPermissionRationale(
50+
context, Manifest.permission.POST_NOTIFICATIONS)) {
51+
ActivityCompat.requestPermissions(
52+
context,
53+
new String[]{Manifest.permission.POST_NOTIFICATIONS},
54+
REQUEST_POST_NOTIFICATIONS
55+
);
56+
} else {
57+
ActivityCompat.requestPermissions(
58+
context,
59+
new String[]{Manifest.permission.POST_NOTIFICATIONS},
60+
REQUEST_POST_NOTIFICATIONS
61+
);
62+
}
63+
}
64+
}
65+
3266
@Override
3367
public void initialize(CordovaInterface cordova, CordovaWebView webView) {
3468
super.initialize(cordova, webView);
3569
this.context = cordova.getContext();
70+
this.activity = cordova.getActivity();
71+
askNotificationPermission(activity);
3672
bindService();
3773
}
3874

3975
private void bindService() {
4076
Intent intent = new Intent(context, TerminalService.class);
41-
context.startService(intent); // Ensure service is started
77+
context.startService(intent);
4278
context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
4379
}
4480

@@ -47,7 +83,7 @@ private void bindService() {
4783
public void onServiceConnected(ComponentName name, IBinder service) {
4884
serviceMessenger = new Messenger(service);
4985
isServiceBound = true;
50-
serviceConnectedLatch.countDown(); // Signal that service is connected
86+
serviceConnectedLatch.countDown();
5187
}
5288

5389
@Override
@@ -103,7 +139,6 @@ public void handleMessage(Message msg) {
103139

104140
@Override
105141
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
106-
// Wait for service to be bound with a timeout
107142
try {
108143
if (!isServiceBound && !serviceConnectedLatch.await(5, TimeUnit.SECONDS)) {
109144
callbackContext.error("Service not bound - timeout");
@@ -177,7 +212,6 @@ private void startProcess(String pid, String cmd, String alpine) {
177212
try {
178213
serviceMessenger.send(msg);
179214
} catch (RemoteException e) {
180-
// Remove the redeclaration of callbackContext here
181215
CallbackContext errorContext = getCallbackContext(pid);
182216
if (errorContext != null) {
183217
errorContext.error("Failed to start process: " + e.getMessage());

src/plugins/terminal/src/android/TerminalService.java

Lines changed: 102 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@
33
import android.app.Notification;
44
import android.app.NotificationChannel;
55
import android.app.NotificationManager;
6+
import android.app.PendingIntent;
67
import android.app.Service;
8+
import android.content.Context;
79
import android.content.Intent;
810
import android.os.Build;
911
import android.os.Bundle;
1012
import android.os.Handler;
1113
import android.os.IBinder;
1214
import android.os.Message;
1315
import android.os.Messenger;
16+
import android.os.PowerManager;
1417
import android.os.RemoteException;
1518
import androidx.core.app.NotificationCompat;
1619
import com.foxdebug.acode.R;
@@ -22,6 +25,8 @@
2225
import java.util.Map;
2326
import java.util.concurrent.ConcurrentHashMap;
2427
import java.util.concurrent.Executors;
28+
import java.lang.reflect.Field;
29+
2530

2631
public class TerminalService extends Service {
2732

@@ -32,19 +37,40 @@ public class TerminalService extends Service {
3237
public static final int MSG_EXEC = 5;
3338

3439
public static final String CHANNEL_ID = "terminal_exec_channel";
40+
41+
public static final String ACTION_EXIT_SERVICE = "com.foxdebug.acode.ACTION_EXIT_SERVICE";
42+
public static final String ACTION_TOGGLE_WAKE_LOCK = "com.foxdebug.acode.ACTION_TOGGLE_WAKE_LOCK";
3543

3644
private final Map<String, Process> processes = new ConcurrentHashMap<>();
3745
private final Map<String, OutputStream> processInputs = new ConcurrentHashMap<>();
3846
private final Map<String, Messenger> clientMessengers = new ConcurrentHashMap<>();
3947
private final java.util.concurrent.ExecutorService threadPool = Executors.newCachedThreadPool();
4048

4149
private final Messenger serviceMessenger = new Messenger(new ServiceHandler());
50+
51+
private PowerManager.WakeLock wakeLock;
52+
private boolean isWakeLockHeld = false;
4253

4354
@Override
4455
public IBinder onBind(Intent intent) {
4556
return serviceMessenger.getBinder();
4657
}
4758

59+
@Override
60+
public int onStartCommand(Intent intent, int flags, int startId) {
61+
if (intent != null) {
62+
String action = intent.getAction();
63+
if (ACTION_EXIT_SERVICE.equals(action)) {
64+
stopForeground(true);
65+
stopSelf();
66+
return START_NOT_STICKY;
67+
} else if (ACTION_TOGGLE_WAKE_LOCK.equals(action)) {
68+
toggleWakeLock();
69+
}
70+
}
71+
return START_STICKY;
72+
}
73+
4874
private class ServiceHandler extends Handler {
4975
@Override
5076
public void handleMessage(Message msg) {
@@ -79,13 +105,40 @@ public void handleMessage(Message msg) {
79105
}
80106
}
81107

108+
private void toggleWakeLock() {
109+
if (isWakeLockHeld) {
110+
releaseWakeLock();
111+
} else {
112+
acquireWakeLock();
113+
}
114+
updateNotification();
115+
}
116+
117+
private void acquireWakeLock() {
118+
if (wakeLock == null) {
119+
PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
120+
wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "AcodeTerminal:WakeLock");
121+
}
122+
123+
if (!isWakeLockHeld) {
124+
wakeLock.acquire();
125+
isWakeLockHeld = true;
126+
}
127+
}
128+
129+
private void releaseWakeLock() {
130+
if (wakeLock != null && isWakeLockHeld) {
131+
wakeLock.release();
132+
isWakeLockHeld = false;
133+
}
134+
}
135+
82136
private void startProcess(String pid, String cmd, String alpine) {
83137
threadPool.execute(() -> {
84138
try {
85139
String xcmd = alpine.equals("true") ? "source $PREFIX/init-sandbox.sh " + cmd : cmd;
86140
ProcessBuilder builder = new ProcessBuilder("sh", "-c", xcmd);
87141

88-
// Set environment variables
89142
Map<String, String> env = builder.environment();
90143
env.put("PREFIX", getFilesDir().getAbsolutePath());
91144
env.put("NATIVE_DIR", getApplicationInfo().nativeLibraryDir);
@@ -100,14 +153,8 @@ private void startProcess(String pid, String cmd, String alpine) {
100153
Process process = builder.start();
101154
processes.put(pid, process);
102155
processInputs.put(pid, process.getOutputStream());
103-
104-
// Stream stdout
105156
threadPool.execute(() -> streamOutput(process.getInputStream(), pid, "stdout"));
106-
107-
// Stream stderr
108157
threadPool.execute(() -> streamOutput(process.getErrorStream(), pid, "stderr"));
109-
110-
// Wait for process to complete
111158
threadPool.execute(() -> {
112159
try {
113160
int exitCode = process.waitFor();
@@ -131,8 +178,6 @@ private void exec(String execId, String cmd, String alpine) {
131178
try {
132179
String xcmd = alpine.equals("true") ? "source $PREFIX/init-sandbox.sh " + cmd : cmd;
133180
ProcessBuilder builder = new ProcessBuilder("sh", "-c", xcmd);
134-
135-
// Set environment variables
136181
Map<String, String> env = builder.environment();
137182
env.put("PREFIX", getFilesDir().getAbsolutePath());
138183
env.put("NATIVE_DIR", getApplicationInfo().nativeLibraryDir);
@@ -145,8 +190,6 @@ private void exec(String execId, String cmd, String alpine) {
145190
}
146191

147192
Process process = builder.start();
148-
149-
// Capture stdout
150193
BufferedReader stdOutReader = new BufferedReader(
151194
new InputStreamReader(process.getInputStream()));
152195
StringBuilder stdOut = new StringBuilder();
@@ -155,7 +198,6 @@ private void exec(String execId, String cmd, String alpine) {
155198
stdOut.append(line).append("\n");
156199
}
157200

158-
// Capture stderr
159201
BufferedReader stdErrReader = new BufferedReader(
160202
new InputStreamReader(process.getErrorStream()));
161203
StringBuilder stdErr = new StringBuilder();
@@ -205,7 +247,6 @@ private void sendMessageToClient(String id, String action, String data) {
205247
msg.setData(bundle);
206248
clientMessenger.send(msg);
207249
} catch (RemoteException e) {
208-
// Client is no longer available, clean up
209250
cleanup(id);
210251
}
211252
}
@@ -224,7 +265,6 @@ private void sendExecResultToClient(String id, boolean isSuccess, String data) {
224265
msg.setData(bundle);
225266
clientMessenger.send(msg);
226267
} catch (RemoteException e) {
227-
// Client is no longer available, clean up
228268
cleanup(id);
229269
}
230270
}
@@ -242,9 +282,23 @@ private void writeToProcess(String pid, String input) {
242282
}
243283
}
244284

285+
private long getPid(Process process) {
286+
try {
287+
Field f = process.getClass().getDeclaredField("pid");
288+
f.setAccessible(true);
289+
return f.getLong(process);
290+
} catch (Exception e) {
291+
return -1;
292+
}
293+
}
294+
295+
245296
private void stopProcess(String pid) {
246297
Process process = processes.get(pid);
247298
if (process != null) {
299+
try {
300+
Runtime.getRuntime().exec("kill -9 -" + getPid(process));
301+
} catch (Exception ignored) {}
248302
process.destroy();
249303
cleanup(pid);
250304
}
@@ -275,13 +329,7 @@ private void cleanup(String id) {
275329
public void onCreate() {
276330
super.onCreate();
277331
createNotificationChannel();
278-
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
279-
.setContentTitle("Acode Service")
280-
.setContentText("Executor service")
281-
.setSmallIcon(R.drawable.ic_launcher_foreground)
282-
.setOngoing(true)
283-
.build();
284-
startForeground(1, notification);
332+
updateNotification();
285333
}
286334

287335
private void createNotificationChannel() {
@@ -298,13 +346,44 @@ private void createNotificationChannel() {
298346
}
299347
}
300348

349+
private void updateNotification() {
350+
Intent exitIntent = new Intent(this, TerminalService.class);
351+
exitIntent.setAction(ACTION_EXIT_SERVICE);
352+
PendingIntent exitPendingIntent = PendingIntent.getService(this, 0, exitIntent,
353+
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
354+
355+
Intent wakeLockIntent = new Intent(this, TerminalService.class);
356+
wakeLockIntent.setAction(ACTION_TOGGLE_WAKE_LOCK);
357+
PendingIntent wakeLockPendingIntent = PendingIntent.getService(this, 1, wakeLockIntent,
358+
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
359+
360+
String contentText = "Executor service" + (isWakeLockHeld ? " (wakelock held)" : "");
361+
String wakeLockButtonText = isWakeLockHeld ? "Release Wake Lock" : "Acquire Wake Lock";
362+
363+
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
364+
.setContentTitle("Acode Service")
365+
.setContentText(contentText)
366+
.setSmallIcon(R.drawable.ic_launcher_foreground)
367+
.setOngoing(true)
368+
.addAction(R.drawable.ic_launcher_foreground, wakeLockButtonText, wakeLockPendingIntent)
369+
.addAction(R.drawable.ic_launcher_foreground, "Exit", exitPendingIntent)
370+
.build();
371+
372+
startForeground(1, notification);
373+
}
374+
301375
@Override
302376
public void onDestroy() {
303377
super.onDestroy();
304-
// Clean up all processes when service is destroyed
378+
releaseWakeLock();
379+
305380
for (Process process : processes.values()) {
306-
process.destroy();
381+
try {
382+
Runtime.getRuntime().exec("kill -9 -" + getPid(process));
383+
} catch (Exception ignored) {}
384+
process.destroyForcibly();
307385
}
386+
308387
processes.clear();
309388
processInputs.clear();
310389
clientMessengers.clear();

0 commit comments

Comments
 (0)