feat(android): Improve background streaming service reliability
This commit is contained in:
@@ -13,7 +13,6 @@ import android.os.Build
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.plugin.common.MethodCall
|
import io.flutter.plugin.common.MethodCall
|
||||||
@@ -27,6 +26,7 @@ import org.json.JSONObject
|
|||||||
class BackgroundStreamingService : Service() {
|
class BackgroundStreamingService : Service() {
|
||||||
private var wakeLock: PowerManager.WakeLock? = null
|
private var wakeLock: PowerManager.WakeLock? = null
|
||||||
private val activeStreams = mutableSetOf<String>()
|
private val activeStreams = mutableSetOf<String>()
|
||||||
|
private var activeStreamCount = 0
|
||||||
private var isForeground = false
|
private var isForeground = false
|
||||||
private var currentForegroundType: Int = 0
|
private var currentForegroundType: Int = 0
|
||||||
private var foregroundStartTime: Long = 0
|
private var foregroundStartTime: Long = 0
|
||||||
@@ -37,49 +37,82 @@ class BackgroundStreamingService : Service() {
|
|||||||
const val ACTION_START = "START_STREAMING"
|
const val ACTION_START = "START_STREAMING"
|
||||||
const val ACTION_STOP = "STOP_STREAMING"
|
const val ACTION_STOP = "STOP_STREAMING"
|
||||||
const val EXTRA_REQUIRES_MICROPHONE = "requiresMicrophone"
|
const val EXTRA_REQUIRES_MICROPHONE = "requiresMicrophone"
|
||||||
|
const val EXTRA_STREAM_COUNT = "streamCount"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
println("BackgroundStreamingService: Service created")
|
println("BackgroundStreamingService: Service created")
|
||||||
|
|
||||||
|
// Enter foreground immediately to satisfy Android's 5s requirement even
|
||||||
|
// if onStartCommand handling is delayed or cancelled.
|
||||||
|
val initialType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
if (!isForeground) {
|
||||||
|
val notification = createMinimalNotification()
|
||||||
|
startForegroundInternal(notification, initialType)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
when (intent?.action) {
|
val action = intent?.action
|
||||||
ACTION_STOP -> {
|
val incomingStreamCount =
|
||||||
stopStreaming()
|
intent?.getIntExtra(EXTRA_STREAM_COUNT, 0) ?: 0
|
||||||
return START_NOT_STICKY
|
activeStreamCount = incomingStreamCount
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// For KEEP_ALIVE, only refresh the wake lock without restarting foreground
|
|
||||||
if (intent?.action == "KEEP_ALIVE") {
|
|
||||||
keepAlive()
|
|
||||||
return START_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
val notification = createMinimalNotification()
|
|
||||||
val desiredType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
val desiredType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
resolveForegroundServiceType(intent)
|
resolveForegroundServiceType(intent)
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isForeground) {
|
// Always enter foreground as early as possible to avoid the 5s timeout
|
||||||
if (!startForegroundInternal(notification, desiredType)) {
|
// even when stop/keep-alive races deliver a STOP intent first.
|
||||||
|
val needsTypeUpdate = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
|
||||||
|
currentForegroundType != desiredType
|
||||||
|
|
||||||
|
if (!isForeground || needsTypeUpdate) {
|
||||||
|
val notification = createMinimalNotification()
|
||||||
|
val enteredForeground = if (!isForeground) {
|
||||||
|
startForegroundInternal(notification, desiredType)
|
||||||
|
} else {
|
||||||
|
updateForegroundType(notification, desiredType)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!enteredForeground) {
|
||||||
stopSelf()
|
stopSelf()
|
||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
|
|
||||||
currentForegroundType != desiredType
|
// If no streams are active after entering foreground, shut down to
|
||||||
) {
|
// avoid lingering foreground instances that could trigger
|
||||||
updateForegroundType(notification, desiredType)
|
// DidNotStopInTime exceptions.
|
||||||
|
if (activeStreamCount <= 0) {
|
||||||
|
stopStreaming()
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
when (intent?.action) {
|
when (action) {
|
||||||
|
ACTION_STOP -> {
|
||||||
|
stopStreaming()
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
"KEEP_ALIVE" -> {
|
||||||
|
keepAlive()
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
ACTION_START -> {
|
ACTION_START -> {
|
||||||
|
if (activeStreamCount > 0) {
|
||||||
acquireWakeLock()
|
acquireWakeLock()
|
||||||
println("BackgroundStreamingService: Started foreground service")
|
println("BackgroundStreamingService: Started foreground service")
|
||||||
|
} else {
|
||||||
|
println("BackgroundStreamingService: No active streams; skipping wake lock")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,6 +182,8 @@ class BackgroundStreamingService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun createMinimalNotification(): Notification {
|
private fun createMinimalNotification(): Notification {
|
||||||
|
ensureNotificationChannel()
|
||||||
|
|
||||||
// Create a minimal, silent notification (required for foreground service)
|
// Create a minimal, silent notification (required for foreground service)
|
||||||
return NotificationCompat.Builder(this, CHANNEL_ID)
|
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
.setContentTitle("Conduit")
|
.setContentTitle("Conduit")
|
||||||
@@ -163,6 +198,29 @@ class BackgroundStreamingService : Service() {
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ensureNotificationChannel() {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||||
|
|
||||||
|
val manager =
|
||||||
|
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
if (manager.getNotificationChannel(CHANNEL_ID) != null) return
|
||||||
|
|
||||||
|
val channel = NotificationChannel(
|
||||||
|
CHANNEL_ID,
|
||||||
|
"Background Service",
|
||||||
|
NotificationManager.IMPORTANCE_MIN,
|
||||||
|
).apply {
|
||||||
|
description = "Background service for Conduit"
|
||||||
|
setShowBadge(false)
|
||||||
|
enableLights(false)
|
||||||
|
enableVibration(false)
|
||||||
|
setSound(null, null)
|
||||||
|
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
|
||||||
private fun acquireWakeLock() {
|
private fun acquireWakeLock() {
|
||||||
if (wakeLock?.isHeld == true) return
|
if (wakeLock?.isHeld == true) return
|
||||||
|
|
||||||
@@ -189,6 +247,11 @@ class BackgroundStreamingService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun keepAlive() {
|
private fun keepAlive() {
|
||||||
|
if (activeStreamCount <= 0) {
|
||||||
|
stopStreaming()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we're approaching Android 14's 6-hour dataSync limit
|
// Check if we're approaching Android 14's 6-hour dataSync limit
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && isForeground) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && isForeground) {
|
||||||
val uptime = System.currentTimeMillis() - foregroundStartTime
|
val uptime = System.currentTimeMillis() - foregroundStartTime
|
||||||
@@ -210,6 +273,7 @@ class BackgroundStreamingService : Service() {
|
|||||||
private fun stopStreaming() {
|
private fun stopStreaming() {
|
||||||
println("BackgroundStreamingService: Stopping service...")
|
println("BackgroundStreamingService: Stopping service...")
|
||||||
activeStreams.clear()
|
activeStreams.clear()
|
||||||
|
activeStreamCount = 0
|
||||||
releaseWakeLock()
|
releaseWakeLock()
|
||||||
|
|
||||||
if (isForeground) {
|
if (isForeground) {
|
||||||
@@ -238,8 +302,21 @@ class BackgroundStreamingService : Service() {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
println("BackgroundStreamingService: onDestroy called")
|
println("BackgroundStreamingService: onDestroy called")
|
||||||
|
if (isForeground) {
|
||||||
|
try {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
stopForeground(true)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("BackgroundStreamingService: Error stopping foreground in onDestroy: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
releaseWakeLock()
|
releaseWakeLock()
|
||||||
activeStreams.clear()
|
activeStreams.clear()
|
||||||
|
activeStreamCount = 0
|
||||||
isForeground = false
|
isForeground = false
|
||||||
foregroundStartTime = 0
|
foregroundStartTime = 0
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
@@ -382,7 +459,10 @@ class BackgroundStreamingHandler(private val activity: MainActivity) : MethodCal
|
|||||||
private fun startForegroundService() {
|
private fun startForegroundService() {
|
||||||
try {
|
try {
|
||||||
val serviceIntent = Intent(context, BackgroundStreamingService::class.java)
|
val serviceIntent = Intent(context, BackgroundStreamingService::class.java)
|
||||||
serviceIntent.putExtra("streamCount", activeStreams.size)
|
serviceIntent.putExtra(
|
||||||
|
BackgroundStreamingService.EXTRA_STREAM_COUNT,
|
||||||
|
activeStreams.size,
|
||||||
|
)
|
||||||
serviceIntent.putExtra(
|
serviceIntent.putExtra(
|
||||||
BackgroundStreamingService.EXTRA_REQUIRES_MICROPHONE,
|
BackgroundStreamingService.EXTRA_REQUIRES_MICROPHONE,
|
||||||
streamsRequiringMic.isNotEmpty(),
|
streamsRequiringMic.isNotEmpty(),
|
||||||
@@ -454,7 +534,10 @@ class BackgroundStreamingHandler(private val activity: MainActivity) : MethodCal
|
|||||||
try {
|
try {
|
||||||
val serviceIntent = Intent(context, BackgroundStreamingService::class.java)
|
val serviceIntent = Intent(context, BackgroundStreamingService::class.java)
|
||||||
serviceIntent.action = "KEEP_ALIVE"
|
serviceIntent.action = "KEEP_ALIVE"
|
||||||
serviceIntent.putExtra("streamCount", activeStreams.size)
|
serviceIntent.putExtra(
|
||||||
|
BackgroundStreamingService.EXTRA_STREAM_COUNT,
|
||||||
|
activeStreams.size,
|
||||||
|
)
|
||||||
serviceIntent.putExtra(
|
serviceIntent.putExtra(
|
||||||
BackgroundStreamingService.EXTRA_REQUIRES_MICROPHONE,
|
BackgroundStreamingService.EXTRA_REQUIRES_MICROPHONE,
|
||||||
streamsRequiringMic.isNotEmpty(),
|
streamsRequiringMic.isNotEmpty(),
|
||||||
|
|||||||
Reference in New Issue
Block a user