I'm looking for either a permissively licensed Android plugin similar to flutter_background_service, but one that only starts an application's foreground service on reboot, app upgrade, etc; or a simple way to do this with something like a MethodChannel
in Kotlin or Java.
The reason I would prefer not to use flutter_background_service
at this time is because it requires communication via serializable data structures between isolates, and it does much more than I need, like starting foreground notifications, a behavior my code already handles via flutter_local_notifications. I was looking at building a custom solution based on code from that plugin, but I don't know much about Android development, and the code is quite complex.
https://stackoverflow.com/a/70980729/582326 works, but it displays the app's main screen on boot. I'd rather not make my app that invasive, so I'm hoping to only launch a foreground service via a sticky notification.
flutter_boot_receiver seems like it would be the right fit, except for its copyleft license. I would prefer not to use that, because I'm working an open source app under the MIT license. (see old answer)
I've found help on Stack Exchange including the following: https://stackoverflow.com/a/78336034/582326. However I already have an isolate in Dart that starts a foreground service, and I don't know how to complete step 4 of that answer without using flutter_background_service
.
From what I understand, I may have to open a MethodChannel
to call Flutter code from Java / Kotlin. The Flutter platform-channels documentation explains how to do something similar, but only in the other (and more commonly used) direction. That, and what to call in Java / Kotlin, is where I'm currently stuck.
Pointers to documentation, method calls, or code for calling a Flutter isolate on boot / after an app upgrade would be much appreciated.
I decided to switch to handling this behavior manually, rather than using an existing module. This allows me to resolve some issues I was running into in a simple way.
To restart the app automatically, we need to start the Flutter engine when one of the following intents is sent by the system.
BootReceiver.kt
:
package com.example.foo
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context : Context, intent : Intent) {
if (intent.action.equals("android.intent.action.BOOT_COMPLETED") ||
intent.action.equals("android.intent.action.QUICKBOOT_POWERON") ||
intent.action.equals("com.htc.intent.action.QUICKBOOT_POWERON") ||
intent.action.equals("android.intent.action.MY_PACKAGE_REPLACED")) {
StartFlutterOnce(context, true)
}
}
}
We also start the Flutter engine if / when the app is opened manually.
MainActivity.kt
:
package com.example.foo
import android.content.Context
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
class MainActivity: FlutterActivity() {
override fun provideFlutterEngine(context: Context): FlutterEngine {
return StartFlutterOnce(context, false).flutterEngine
}
}
We also want to only run one FlutterEngine at a time. That's because opening multiple of these will likely waste memory, and could lead to multiple background processes running jobs that don't get used for any real purpose. It appears that another isolate can't be stopped from within another, so we do it in Kotlin.
Furthermore, when the BootReceiver is called by the system after a reboot, the app doesn't have permission to display the full app yet. That seems to be a feature of the Context
variable that gets passed in. So when the app is started by the user, we first need to clean up the old background FlutterEngine
and DartExecutor
.
This code also works around a bug in Android SDK 35 where the "boot complete" gets delivered to the app every time that it is started by the user, which would create multiple isolates again.
StartFlutterOnce.kt
:
package com.example.foo
import android.content.Context
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineCache
import io.flutter.embedding.engine.FlutterEngineGroup
import io.flutter.embedding.engine.dart.DartExecutor
class StartFlutterOnce (context: Context, backgroundOnly: Boolean) {
private val bgEngineName: String = "single_bg_engine"
private val fgEngineName: String = "single_fg_engine"
lateinit var flutterEngine: FlutterEngine
init {
val engineName: String
if (backgroundOnly) {
engineName = bgEngineName
} else {
FlutterEngineCache.getInstance().get(bgEngineName)?.destroy()
engineName = fgEngineName
}
if (FlutterEngineCache.getInstance().get(engineName) == null) {
val group = FlutterEngineGroup(context)
flutterEngine = group.createAndRunDefaultEngine(context)
flutterEngine.dartExecutor.executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault())
FlutterEngineCache.getInstance().put(engineName, flutterEngine)
}
}
}
I'm using the flutter_local_notifications
package, so I have that in the AndroidManifest, along with the code for the BootReceiver. The intent filter may be optional here, because it's checked by the app itself. This code also restarts the app if it's been upgraded by the system or via adb install
.
AndroidManifest.xml
:
<service
android:name="com.dexterous.flutterlocalnotifications.ForegroundService"
android:exported="false"
android:stopWithTask="false"
android:foregroundServiceType="dataSync" />
<receiver
android:name=".BootReceiver"
android:enabled="true"
android:exported="true"
android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
Here we call Dart code to start the app. I have it broken out in two functions, but you don't have to do that. If the app is started in the background after a boot, the app won't get drawn on the screen until the user launches it manually. Dart seems to handle this gracefully, so the app manages to run properly in the background without running foreground code.
Note that I'm using flutter_local_notifications
, and I am using .startForegroundService()
from that package to start the notification that keeps the background process running.
To get that setup working for me, I ended up moving all code that should run in the background into its own background isolate, using https://dart.dev/language/isolates#complete-example as a starting point to bridge the two isolates. Communication between the main and background isolates happens by sending enums for method names, plus arguments. The messages are sent via ports set up in that tutorial.
Note that you'll still need to ask for permission to show notifications from the main Flutter isolate before you can show the notification for a "foreground service" (a background service with a sticky notification in the foreground).
main.dart
:
Future<void> main() async {
await startBackground(); // custom dart code
await startForeground(); // custom dart code
}