Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package de.julianassmann.flutter_background
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Handler
import android.os.Looper
import androidx.annotation.NonNull
import androidx.core.app.NotificationCompat

Expand Down Expand Up @@ -56,6 +58,63 @@ class FlutterBackgroundPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
@JvmStatic
var shouldRequestBatteryOptimizationsOff: Boolean = true

private const val SERVICE_START_TIMEOUT_MS = 5000L
private val mainHandler = Handler(Looper.getMainLooper())
private var pendingEnableResult: Result? = null
private var pendingEnableRequestId = 0

@JvmStatic
var isBackgroundExecutionRunning: Boolean = false
private set

@JvmStatic
fun preparePendingEnableResult(result: Result): Boolean {
if (pendingEnableResult != null) {
return false
}

pendingEnableRequestId += 1
val requestId = pendingEnableRequestId
pendingEnableResult = result
mainHandler.postDelayed({
if (pendingEnableResult != null && pendingEnableRequestId == requestId) {
val pending = pendingEnableResult
pendingEnableResult = null
isBackgroundExecutionRunning = false
pending?.error(
"ForegroundServiceStartTimeout",
"Timed out waiting for the foreground service to start.",
null
)
}
}, SERVICE_START_TIMEOUT_MS)
return true
}

@JvmStatic
fun notifyServiceStarted() {
isBackgroundExecutionRunning = true
val pending = pendingEnableResult
pendingEnableResult = null
pending?.success(true)
}

@JvmStatic
fun notifyServiceStartFailed(error: Throwable) {
isBackgroundExecutionRunning = false
val pending = pendingEnableResult
pendingEnableResult = null
pending?.error(
"ForegroundServiceStartError",
"Unable to start the foreground service: ${error.message}",
error.toString()
)
}

@JvmStatic
fun notifyServiceStopped() {
isBackgroundExecutionRunning = false
}

fun loadNotificationConfiguration(context: Context?) {
val sharedPref = context?.getSharedPreferences(context.packageName + "_preferences", Context.MODE_PRIVATE)
Expand Down Expand Up @@ -150,32 +209,61 @@ class FlutterBackgroundPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
}
}
"enableBackgroundExecution" -> {
val appContext = context
if (appContext == null) {
result.error("NoContextError", "The plugin is not attached to an application context.", "")
return
}

// Ensure all the necessary permissions are granted
if (!permissionHandler!!.isWakeLockPermissionGranted()) {
result.error("PermissionError", "Please add the WAKE_LOCK permission to the AndroidManifest.xml in order to use background_sockets.", "")
return
} else if (shouldRequestBatteryOptimizationsOff && !permissionHandler!!.isIgnoringBatteryOptimizations()) {
result.error("PermissionError", "The battery optimizations are not turned off.", "")
} else {
if (isBackgroundExecutionRunning) {
result.success(true)
return
}

if (!preparePendingEnableResult(result)) {
result.error("ForegroundServiceStartInProgress", "The foreground service is already starting.", "")
return
}

val intent = Intent(context, IsolateHolderService::class.java)
intent.action = IsolateHolderService.ACTION_START
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
context!!.startForegroundService(intent)
} else {
context!!.startService(intent)
try {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
appContext.startForegroundService(intent)
} else {
appContext.startService(intent)
}
} catch (e: Exception) {
notifyServiceStartFailed(e)
}
result.success(true)
}
}
"disableBackgroundExecution" -> {
val intent = Intent(context!!, IsolateHolderService::class.java)
intent.action = IsolateHolderService.ACTION_SHUTDOWN
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
context!!.startForegroundService(intent)
} else {
context!!.startService(intent)
val appContext = context
if (appContext == null) {
result.error("NoContextError", "The plugin is not attached to an application context.", "")
return
}

val intent = Intent(appContext, IsolateHolderService::class.java)
try {
appContext.stopService(intent)
notifyServiceStopped()
result.success(true)
} catch (e: Exception) {
result.error(
"ForegroundServiceStopError",
"Unable to stop the foreground service: ${e.message}",
e.toString()
)
}
result.success(true)
}
else -> {
result.notImplemented()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package de.julianassmann.flutter_background

import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
Expand All @@ -12,6 +13,7 @@ import android.net.wifi.WifiManager
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import android.util.Log
import androidx.core.app.NotificationCompat

class IsolateHolderService : Service() {
Expand Down Expand Up @@ -50,18 +52,30 @@ class IsolateHolderService : Service() {
if (intent?.action == ACTION_SHUTDOWN) {
cleanupService()
stopSelf()
return START_NOT_STICKY
} else if (intent?.action == ACTION_START) {
startService()
return if (startService()) START_STICKY else START_NOT_STICKY
}
return START_STICKY
return START_NOT_STICKY
}

private fun cleanupService() {
cleanupLocks()
try {
stopForeground(true)
} catch (e: Exception) {
Log.w(TAG, "Unable to stop foreground service cleanly", e)
}
FlutterBackgroundPlugin.notifyServiceStopped()
}

private fun cleanupLocks() {
wakeLock?.apply {
if (isHeld) {
release()
}
}
wakeLock = null

if (FlutterBackgroundPlugin.enableWifiLock) {
wifiLock?.apply {
Expand All @@ -70,12 +84,11 @@ class IsolateHolderService : Service() {
}
}
}

stopForeground(true)
wifiLock = null
}

@SuppressLint("WakelockTimeout")
private fun startService() {
private fun startService(): Boolean {
val pm = applicationContext.packageManager
val notificationIntent =
pm.getLaunchIntentForPackage(applicationContext.packageName)
Expand Down Expand Up @@ -112,28 +125,59 @@ class IsolateHolderService : Service() {
.setPriority(FlutterBackgroundPlugin.notificationImportance)
.build()

if (!startForegroundSafely(notification)) {
return false
}

acquireLocks()
FlutterBackgroundPlugin.notifyServiceStarted()
return true
}

@SuppressLint("WakelockTimeout")
private fun acquireLocks() {
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
wakeLock = newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG).apply {
setReferenceCounted(false)
acquire()
}
}

(applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager).run {
wifiLock = createWifiLock(WifiManager.WIFI_MODE_FULL, WIFILOCK_TAG).apply {
setReferenceCounted(false)
acquire()
if (FlutterBackgroundPlugin.enableWifiLock) {
(applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager).run {
wifiLock = createWifiLock(WifiManager.WIFI_MODE_FULL, WIFILOCK_TAG).apply {
setReferenceCounted(false)
acquire()
}
}
}
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
// Use all foreground service types set in the manifest file
startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST);
} else {
startForeground(1, notification)
private fun startForegroundSafely(notification: Notification): Boolean {
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
// Use all foreground service types set in the manifest file
startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST)
} else {
startForeground(1, notification)
}
true
} catch (e: SecurityException) {
handleStartFailure(e)
false
} catch (e: RuntimeException) {
handleStartFailure(e)
false
}
}

private fun handleStartFailure(error: Throwable) {
Log.e(TAG, "Unable to start foreground service", error)
cleanupLocks()
FlutterBackgroundPlugin.notifyServiceStartFailed(error)
stopSelf()
}

override fun onTaskRemoved(rootIntent: Intent) {
super.onTaskRemoved(rootIntent)
cleanupService()
Expand Down
9 changes: 5 additions & 4 deletions lib/src/flutter_background.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,11 @@ class FlutterBackground {
/// May throw a [PlatformException].
static Future<bool> enableBackgroundExecution() async {
if (_isInitialized) {
final success =
await _channel.invokeMethod<bool>('enableBackgroundExecution');
_isBackgroundExecutionEnabled = true;
return success == true;
final success = await _channel.invokeMethod<bool>(
'enableBackgroundExecution',
);
_isBackgroundExecutionEnabled = success == true;
return _isBackgroundExecutionEnabled;
} else {
throw Exception(
'FlutterBackground plugin must be initialized before calling enableBackgroundExecution()');
Expand Down