diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6aa7554fd..0a8262df8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -12,6 +12,8 @@
+
+
@@ -86,5 +88,15 @@
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="keep the IDE process alive while backgrounded" />
+
+
+
+
+
+
+
diff --git a/app/src/main/kotlin/org/cosmicide/service/BootReceiver.kt b/app/src/main/kotlin/org/cosmicide/service/BootReceiver.kt
new file mode 100644
index 000000000..1b90f994f
--- /dev/null
+++ b/app/src/main/kotlin/org/cosmicide/service/BootReceiver.kt
@@ -0,0 +1,31 @@
+/*
+ * This file is part of Cosmic IDE.
+ * Cosmic IDE is a free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ * Cosmic IDE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License along with Cosmic IDE. If not, see .
+ */
+
+package org.cosmicide.service
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+
+/**
+ * Receiver that starts the KeepAliveService on device boot.
+ * Similar to Termux's boot receiver pattern.
+ */
+class BootReceiver : BroadcastReceiver() {
+
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
+ Log.i(TAG, "Boot completed, starting KeepAliveService")
+ KeepAliveService.start(context)
+ }
+ }
+
+ companion object {
+ private const val TAG = "BootReceiver"
+ }
+}
diff --git a/app/src/main/kotlin/org/cosmicide/service/KeepAliveService.kt b/app/src/main/kotlin/org/cosmicide/service/KeepAliveService.kt
index 75175179f..c031917f1 100644
--- a/app/src/main/kotlin/org/cosmicide/service/KeepAliveService.kt
+++ b/app/src/main/kotlin/org/cosmicide/service/KeepAliveService.kt
@@ -17,61 +17,193 @@ import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
+import android.os.PowerManager
+import android.util.Log
import org.cosmicide.MainActivity
import org.cosmicide.R
/**
* Foreground service that keeps the app process alive in the background, like Termux.
+ *
+ * Features:
+ * - Foreground service with persistent notification
+ * - WakeLock support to prevent CPU from sleeping
+ * - Notification actions (stop service)
+ * - Auto-restart with START_STICKY
+ * - Proper cleanup on destroy
*/
class KeepAliveService : Service() {
+ private var wakeLock: PowerManager.WakeLock? = null
+ private var isServiceStopping = false
+
override fun onCreate() {
super.onCreate()
- startForegroundCompat()
+ Log.v(TAG, "onCreate")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
- startForeground(NotificationId, buildNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
- } else {
- startForeground(NotificationId, buildNotification())
+ Log.v(TAG, "onStartCommand action=${intent?.action}")
+
+ when (intent?.action) {
+ ACTION_STOP -> {
+ actionStopService()
+ return START_NOT_STICKY
+ }
+ ACTION_ACQUIRE_WAKE_LOCK -> {
+ actionAcquireWakeLock()
+ }
+ ACTION_RELEASE_WAKE_LOCK -> {
+ actionReleaseWakeLock()
+ }
}
+
+ runStartForeground()
return START_STICKY
}
override fun onBind(intent: Intent?): IBinder? = null
- private fun startForegroundCompat() = onStartCommand(null, 0, 0)
+ override fun onDestroy() {
+ Log.v(TAG, "onDestroy")
+ actionReleaseWakeLock()
+ super.onDestroy()
+ }
+
+ private fun runStartForeground() {
+ setupNotificationChannel()
+ val notification = buildNotification()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
+ } else {
+ startForeground(NOTIFICATION_ID, notification)
+ }
+ }
- private fun buildNotification(): Notification {
- val channelId = "keepalive"
+ private fun runStopForeground() {
+ stopForeground(STOP_FOREGROUND_REMOVE)
+ }
+
+ private fun actionStopService() {
+ isServiceStopping = true
+ actionReleaseWakeLock()
+ runStopForeground()
+ stopSelf()
+ }
+
+ private fun actionAcquireWakeLock() {
+ if (wakeLock == null) {
+ val powerManager = getSystemService(POWER_SERVICE) as PowerManager
+ wakeLock = powerManager.newWakeLock(
+ PowerManager.PARTIAL_WAKE_LOCK,
+ "$packageName:keep_alive"
+ ).apply {
+ acquire(10 * 60 * 1000L) // 10 minutes
+ }
+ }
+ updateNotification()
+ }
+
+ private fun actionReleaseWakeLock() {
+ wakeLock?.let {
+ if (it.isHeld) {
+ it.release()
+ }
+ }
+ wakeLock = null
+ if (!isServiceStopping) {
+ updateNotification()
+ }
+ }
+
+ private fun updateNotification() {
+ val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.notify(NOTIFICATION_ID, buildNotification())
+ }
+
+ private fun setupNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
- channelId, "Foreground service", NotificationManager.IMPORTANCE_MIN
- )
+ CHANNEL_ID,
+ getString(R.string.keep_alive_notification_channel),
+ NotificationManager.IMPORTANCE_MIN
+ ).apply {
+ description = getString(R.string.keep_alive_notification_channel_description)
+ setShowBadge(false)
+ }
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
}
+ }
+ private fun buildNotification(): Notification {
val contentIntent = PendingIntent.getActivity(
this, 0,
- Intent(this, MainActivity::class.java),
+ Intent(this, MainActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
+ },
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+
+ val stopIntent = PendingIntent.getService(
+ this, 0,
+ Intent(this, KeepAliveService::class.java).apply {
+ action = ACTION_STOP
+ },
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+
+ val wakeLockHeld = wakeLock?.isHeld == true
+ val wakeLockText = if (wakeLockHeld) {
+ getString(R.string.notification_action_wake_unlock)
+ } else {
+ getString(R.string.notification_action_wake_lock)
+ }
+ val wakeLockAction = if (wakeLockHeld) ACTION_RELEASE_WAKE_LOCK else ACTION_ACQUIRE_WAKE_LOCK
+
+ val wakeLockIntent = PendingIntent.getService(
+ this, 1,
+ Intent(this, KeepAliveService::class.java).apply {
+ action = wakeLockAction
+ },
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
- return Notification.Builder(this, channelId)
+ val wakeLockIcon = if (wakeLockHeld) {
+ android.R.drawable.ic_lock_idle_lock
+ } else {
+ android.R.drawable.ic_lock_lock
+ }
+
+ return Notification.Builder(this, CHANNEL_ID)
.setContentTitle(getString(R.string.app_name))
.setContentText(getString(R.string.keep_alive_notification_text))
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setOngoing(true)
.setContentIntent(contentIntent)
+ .addAction(wakeLockIcon, wakeLockText, wakeLockIntent)
+ .addAction(android.R.drawable.ic_menu_close_clear_cancel, getString(R.string.notification_action_stop), stopIntent)
.build()
}
companion object {
- private const val NotificationId = 1
+ private const val TAG = "KeepAliveService"
+ private const val CHANNEL_ID = "keepalive"
+ private const val NOTIFICATION_ID = 1
+
+ const val ACTION_STOP = "org.cosmicide.service.action.STOP"
+ const val ACTION_ACQUIRE_WAKE_LOCK = "org.cosmicide.service.action.ACQUIRE_WAKE_LOCK"
+ const val ACTION_RELEASE_WAKE_LOCK = "org.cosmicide.service.action.RELEASE_WAKE_LOCK"
fun start(context: Context) {
context.startForegroundService(Intent(context, KeepAliveService::class.java))
}
+
+ fun stop(context: Context) {
+ context.startService(
+ Intent(context, KeepAliveService::class.java).apply {
+ action = ACTION_STOP
+ }
+ )
+ }
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index cb8b96734..85dab324c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -26,6 +26,11 @@
Terminal
Cosmic IDE is running in the background
+ Foreground service
+ Keeps the IDE process alive while running in the background
+ Acquire wake lock
+ Release wake lock
+ Stop
Copy
Quick fixes