diff --git a/modules/background-downloader/android/src/main/java/expo/modules/backgrounddownloader/DownloadService.kt b/modules/background-downloader/android/src/main/java/expo/modules/backgrounddownloader/DownloadService.kt index ed05fc5c..e7cc01b9 100644 --- a/modules/background-downloader/android/src/main/java/expo/modules/backgrounddownloader/DownloadService.kt +++ b/modules/background-downloader/android/src/main/java/expo/modules/backgrounddownloader/DownloadService.kt @@ -5,42 +5,92 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.app.Service import android.content.Intent +import android.content.pm.ServiceInfo import android.os.Binder import android.os.Build import android.os.IBinder +import android.os.SystemClock import android.util.Log import androidx.core.app.NotificationCompat +import androidx.core.app.ServiceCompat class DownloadService : Service() { private val TAG = "DownloadService" private val NOTIFICATION_ID = 1001 private val CHANNEL_ID = "download_channel" - + + // Time threshold to detect if we're in boot context (10 minutes after boot) + private val BOOT_THRESHOLD_MS = 10 * 60 * 1000L + private val binder = DownloadServiceBinder() private var activeDownloadCount = 0 private var currentDownloadTitle = "Preparing download..." private var currentProgress = 0 - + private var isForegroundStarted = false + inner class DownloadServiceBinder : Binder() { fun getService(): DownloadService = this@DownloadService } - + override fun onCreate() { super.onCreate() Log.d(TAG, "DownloadService created") createNotificationChannel() } - + override fun onBind(intent: Intent?): IBinder { Log.d(TAG, "DownloadService bound") return binder } - + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Log.d(TAG, "DownloadService started") - startForeground(NOTIFICATION_ID, createNotification()) + + // On Android 15+, dataSync foreground services cannot be started from BOOT_COMPLETED context + // Check if we're likely in a boot context and skip foreground start if so + if (Build.VERSION.SDK_INT >= 35 && isLikelyBootContext()) { + Log.w(TAG, "Skipping foreground start - likely boot context on Android 15+") + stopSelf() + return START_NOT_STICKY + } + + startForegroundSafely() return START_STICKY } + + /** + * Check if we're likely in a boot context by checking system uptime. + * If the system has been up for less than the threshold, we might be in boot context. + */ + private fun isLikelyBootContext(): Boolean { + val uptimeMs = SystemClock.elapsedRealtime() + return uptimeMs < BOOT_THRESHOLD_MS + } + + /** + * Start foreground service safely with proper service type for Android 14+ + */ + private fun startForegroundSafely() { + if (isForegroundStarted) return + + try { + if (Build.VERSION.SDK_INT >= 34) { + ServiceCompat.startForeground( + this, + NOTIFICATION_ID, + createNotification(), + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC + ) + } else { + startForeground(NOTIFICATION_ID, createNotification()) + } + isForegroundStarted = true + } catch (e: Exception) { + Log.e(TAG, "Failed to start foreground service", e) + // If we can't start foreground, stop the service + stopSelf() + } + } override fun onDestroy() { Log.d(TAG, "DownloadService destroyed") @@ -86,7 +136,7 @@ class DownloadService : Service() { activeDownloadCount++ Log.d(TAG, "Download started, active count: $activeDownloadCount") if (activeDownloadCount == 1) { - startForeground(NOTIFICATION_ID, createNotification()) + startForegroundSafely() } } @@ -94,7 +144,10 @@ class DownloadService : Service() { activeDownloadCount = maxOf(0, activeDownloadCount - 1) Log.d(TAG, "Download stopped, active count: $activeDownloadCount") if (activeDownloadCount == 0) { - stopForeground(STOP_FOREGROUND_REMOVE) + if (isForegroundStarted) { + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) + isForegroundStarted = false + } stopSelf() } }