Skip to content

Commit 84ae20d

Browse files
committed
fix(android): prevent app auto-termination with foreground service fixes
- Pass FOREGROUND_SERVICE_TYPE_DATA_SYNC to startForeground() on API 34+ - Acquire partial wake lock to keep CPU alive during doze - Add onTaskRemoved() to restart service on swipe-away - Add REQUEST_IGNORE_BATTERY_OPTIMIZATIONS permission - Add native methods to check/request battery optimization exemption - Wire battery optimization button to system exemption dialog
1 parent fb5e4a0 commit 84ae20d

5 files changed

Lines changed: 101 additions & 5 deletions

File tree

App.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { useFonts, Syne_700Bold, Syne_500Medium } from '@expo-google-fonts/syne'
2323
import { Manrope_400Regular, Manrope_600SemiBold } from '@expo-google-fonts/manrope';
2424
import { Ionicons } from '@expo/vector-icons';
2525
import { tamaguiConfig } from './tamagui.config';
26-
import { startWolServer, stopWolServer, getLogs, clearLogs } from './native/WolServerModule';
26+
import { startWolServer, stopWolServer, getLogs, clearLogs, isIgnoringBatteryOptimizations, requestIgnoreBatteryOptimizations } from './native/WolServerModule';
2727

2828
const statusSubscribers = new Set();
2929

@@ -370,12 +370,23 @@ const AppContent = () => {
370370
</YStack>
371371

372372
<Button
373-
onPress={() => { Linking.openSettings(); }}
373+
onPress={async () => {
374+
try {
375+
const isIgnoring = await isIgnoringBatteryOptimizations();
376+
if (isIgnoring) {
377+
Alert.alert('Already Optimized', 'Battery optimization is already disabled for WOLE. The service will run reliably in the background.');
378+
} else {
379+
await requestIgnoreBatteryOptimizations();
380+
}
381+
} catch (e) {
382+
Linking.openSettings();
383+
}
384+
}}
374385
backgroundColor="rgba(255,255,255,0.1)"
375386
color="white"
376387
icon={<Ionicons name="battery-charging-outline" size={18} color="white" />}
377388
>
378-
Battery Optimization Settings
389+
Disable Battery Optimization
379390
</Button>
380391
</YStack>
381392
</GlassCard>

android/app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
99
<uses-permission android:name="android.permission.VIBRATE"/>
1010
<uses-permission android:name="android.permission.WAKE_LOCK"/>
11+
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
1112
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
1213
<queries>
1314
<intent>

android/app/src/main/java/com/anonymous/wolrelay/WolHttpServerService.kt

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,31 @@ import android.app.NotificationManager
66
import android.app.PendingIntent
77
import android.app.Service
88
import android.content.Intent
9+
import android.content.pm.ServiceInfo
910
import android.os.Build
1011
import android.os.IBinder
12+
import android.os.PowerManager
1113
import androidx.core.app.NotificationCompat
1214

1315
class WolHttpServerService : Service() {
1416
private var server: WolServer? = null
17+
private var wakeLock: PowerManager.WakeLock? = null
1518

1619
override fun onCreate() {
1720
super.onCreate()
1821
FileLogger.log(applicationContext, "WolHttpServerService", "onCreate")
22+
acquireWakeLock()
1923
startInForeground()
2024
}
2125

2226
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
2327
val prefs = getSharedPreferences("wol_prefs", MODE_PRIVATE)
2428

29+
val isSystemRestart = intent == null
30+
if (isSystemRestart) {
31+
FileLogger.log(applicationContext, "WolHttpServerService", "onStartCommand: system restart (START_STICKY)")
32+
}
33+
2534
var port = intent?.getIntExtra(EXTRA_PORT, -1) ?: -1
2635
var token = intent?.getStringExtra(EXTRA_TOKEN)
2736

@@ -44,9 +53,22 @@ class WolHttpServerService : Service() {
4453
}
4554

4655
override fun onDestroy() {
47-
super.onDestroy()
4856
FileLogger.log(applicationContext, "WolHttpServerService", "onDestroy")
4957
stopServer()
58+
releaseWakeLock()
59+
super.onDestroy()
60+
}
61+
62+
override fun onTaskRemoved(rootIntent: Intent?) {
63+
FileLogger.log(applicationContext, "WolHttpServerService", "onTaskRemoved: restarting service")
64+
// Re-start ourselves so the service survives swipe-away
65+
val restartIntent = Intent(applicationContext, WolHttpServerService::class.java)
66+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
67+
applicationContext.startForegroundService(restartIntent)
68+
} else {
69+
applicationContext.startService(restartIntent)
70+
}
71+
super.onTaskRemoved(rootIntent)
5072
}
5173

5274
override fun onBind(intent: Intent?): IBinder? = null
@@ -63,6 +85,25 @@ class WolHttpServerService : Service() {
6385
server = null
6486
}
6587

88+
private fun acquireWakeLock() {
89+
if (wakeLock == null) {
90+
val pm = getSystemService(POWER_SERVICE) as PowerManager
91+
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "WOLE::WolServerLock")
92+
wakeLock?.acquire()
93+
FileLogger.log(applicationContext, "WolHttpServerService", "Wake lock acquired")
94+
}
95+
}
96+
97+
private fun releaseWakeLock() {
98+
wakeLock?.let {
99+
if (it.isHeld) {
100+
it.release()
101+
FileLogger.log(applicationContext, "WolHttpServerService", "Wake lock released")
102+
}
103+
}
104+
wakeLock = null
105+
}
106+
66107
private fun startInForeground() {
67108
val channelId = "wol_server_channel"
68109
val nm = getSystemService(NotificationManager::class.java)
@@ -84,7 +125,12 @@ class WolHttpServerService : Service() {
84125
.setOngoing(true)
85126
.build()
86127

87-
startForeground(NOTIFICATION_ID, notification)
128+
// Android 14+ (API 34) requires foregroundServiceType in startForeground()
129+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
130+
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
131+
} else {
132+
startForeground(NOTIFICATION_ID, notification)
133+
}
88134
}
89135

90136
companion object {

android/app/src/main/java/com/anonymous/wolrelay/WolModule.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package com.anonymous.wolrelay
22

33
import android.content.Context
44
import android.content.Intent
5+
import android.net.Uri
6+
import android.os.PowerManager
7+
import android.provider.Settings
58
import com.facebook.react.bridge.Promise
69
import com.facebook.react.bridge.ReactApplicationContext
710
import com.facebook.react.bridge.ReactContextBaseJavaModule
@@ -47,6 +50,31 @@ class WolModule(private val reactContext: ReactApplicationContext) : ReactContex
4750
}
4851
}
4952

53+
@ReactMethod
54+
fun isIgnoringBatteryOptimizations(promise: Promise) {
55+
try {
56+
val pm = reactContext.getSystemService(Context.POWER_SERVICE) as PowerManager
57+
val packageName = reactContext.packageName
58+
promise.resolve(pm.isIgnoringBatteryOptimizations(packageName))
59+
} catch (e: Exception) {
60+
promise.reject("BATTERY_CHECK_ERROR", e)
61+
}
62+
}
63+
64+
@ReactMethod
65+
fun requestIgnoreBatteryOptimizations(promise: Promise) {
66+
try {
67+
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
68+
data = Uri.parse("package:${reactContext.packageName}")
69+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
70+
}
71+
reactContext.startActivity(intent)
72+
promise.resolve(true)
73+
} catch (e: Exception) {
74+
promise.reject("BATTERY_REQUEST_ERROR", e)
75+
}
76+
}
77+
5078
@ReactMethod
5179
fun getLogs(promise: Promise) {
5280
try {

native/WolServerModule.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@ export async function stopWolServer() {
1212
return WolServerModule.stop();
1313
}
1414

15+
export async function isIgnoringBatteryOptimizations() {
16+
if (Platform.OS !== 'android') return true;
17+
return WolServerModule.isIgnoringBatteryOptimizations();
18+
}
19+
20+
export async function requestIgnoreBatteryOptimizations() {
21+
if (Platform.OS !== 'android') return;
22+
return WolServerModule.requestIgnoreBatteryOptimizations();
23+
}
24+
1525
export async function getLogs() {
1626
if (Platform.OS !== 'android') return '';
1727
return WolServerModule.getLogs();

0 commit comments

Comments
 (0)