feat: enhance background streaming service with foreground handling

- Improved BackgroundStreamingService to manage foreground service notifications more effectively, ensuring compliance with Android requirements.
- Implemented dynamic foreground service type resolution based on microphone permission, enhancing service behavior based on app state.
- Added checks for app foreground status in connectivity management, improving responsiveness to network changes.
- Refactored notification handling to streamline service lifecycle management and improve code maintainability.
This commit is contained in:
cogwheel0
2025-10-09 15:47:27 +05:30
parent 162a5e0781
commit c073d71363
4 changed files with 161 additions and 9 deletions

View File

@@ -1,14 +1,20 @@
package app.cogwheel.conduit
import android.app.*
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
@@ -21,22 +27,41 @@ import org.json.JSONObject
class BackgroundStreamingService : Service() {
private var wakeLock: PowerManager.WakeLock? = null
private val activeStreams = mutableSetOf<String>()
private var isForeground = false
private var currentForegroundType: Int = 0
companion object {
const val CHANNEL_ID = "conduit_streaming_channel"
const val NOTIFICATION_ID = 1001
const val ACTION_START = "START_STREAMING"
const val ACTION_STOP = "STOP_STREAMING"
private const val EXTRA_REQUIRES_MICROPHONE = "requiresMicrophone"
}
override fun onCreate() {
super.onCreate()
// Start foreground with minimal notification (required for foreground service)
startForeground(NOTIFICATION_ID, createMinimalNotification())
println("BackgroundStreamingService: Service created")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = createMinimalNotification()
val desiredType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
resolveForegroundServiceType(intent)
} else {
0
}
if (!isForeground) {
if (!startForegroundInternal(notification, desiredType)) {
stopSelf()
return START_NOT_STICKY
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
currentForegroundType != desiredType
) {
updateForegroundType(notification, desiredType)
}
when (intent?.action) {
ACTION_START -> {
acquireWakeLock()
@@ -53,6 +78,55 @@ class BackgroundStreamingService : Service() {
return START_STICKY // Restart if killed by system
}
private fun startForegroundInternal(notification: Notification, type: Int): Boolean {
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(NOTIFICATION_ID, notification, type)
currentForegroundType = type
} else {
@Suppress("DEPRECATION")
startForeground(NOTIFICATION_ID, notification)
}
isForeground = true
true
} catch (e: SecurityException) {
println("BackgroundStreamingService: Failed to enter foreground: ${e.message}")
false
}
}
private fun updateForegroundType(notification: Notification, type: Int) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return
try {
startForeground(NOTIFICATION_ID, notification, type)
currentForegroundType = type
} catch (e: SecurityException) {
println("BackgroundStreamingService: Unable to update foreground type: ${e.message}")
}
}
private fun resolveForegroundServiceType(intent: Intent?): Int {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return 0
val requiresMicrophone = intent?.getBooleanExtra(EXTRA_REQUIRES_MICROPHONE, false) ?: false
if (requiresMicrophone) {
if (hasRecordAudioPermission()) {
return ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE or
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
}
println("BackgroundStreamingService: Microphone permission missing; falling back to data sync type")
}
return ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
}
private fun hasRecordAudioPermission(): Boolean {
return ContextCompat.checkSelfPermission(
this,
android.Manifest.permission.RECORD_AUDIO
) == PackageManager.PERMISSION_GRANTED
}
private fun createMinimalNotification(): Notification {
// Create a minimal, silent notification (required for foreground service)
return NotificationCompat.Builder(this, CHANNEL_ID)
@@ -103,15 +177,17 @@ class BackgroundStreamingService : Service() {
releaseWakeLock()
stopForeground(true)
stopSelf()
isForeground = false
println("BackgroundStreamingService: Service stopped")
}
override fun onDestroy() {
releaseWakeLock()
isForeground = false
super.onDestroy()
println("BackgroundStreamingService: Service destroyed")
}
override fun onBind(intent: Intent?): IBinder? = null
}
@@ -363,4 +439,4 @@ class BackgroundStreamingHandler(private val activity: MainActivity) : MethodCal
stopBackgroundMonitoring()
stopForegroundService()
}
}
}