diff --git a/android/build.gradle b/android/build.gradle
index f9e4677..1048ed9 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -48,5 +48,6 @@ android {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "androidx.activity:activity-ktx:1.7.2"
+ implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
implementation "androidx.datastore:datastore-preferences:1.0.0"
}
\ No newline at end of file
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index 9de0eaa..99186e9 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -4,7 +4,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/src/main/kotlin/org/moxxy/moxxy_native/Constants.kt b/android/src/main/kotlin/org/moxxy/moxxy_native/Constants.kt
index e824349..4fd96cc 100644
--- a/android/src/main/kotlin/org/moxxy/moxxy_native/Constants.kt
+++ b/android/src/main/kotlin/org/moxxy/moxxy_native/Constants.kt
@@ -37,3 +37,16 @@ const val SHARED_PREFERENCES_AVATAR_KEY = "avatar_path"
const val PICK_FILE_REQUEST = 42
const val PICK_FILES_REQUEST = 43
const val PICK_FILE_WITH_DATA_REQUEST = 44
+
+// Service
+const val SERVICE_SHARED_PREFERENCES_KEY = "me.polynom.moxplatform_android"
+const val SERVICE_ENTRYPOINT_KEY = "entrypoint_handle"
+const val SERVICE_EXTRA_DATA_KEY = "extra_data"
+const val SERVICE_START_AT_BOOT_KEY = "auto_start_at_boot"
+const val SERVICE_MANUALLY_STOPPED_KEY = "manually_stopped"
+
+// https://github.com/ekasetiawans/flutter_background_service/blob/e427f3b70138ec26f9671c2617f9061f25eade6f/packages/flutter_background_service_android/android/src/main/java/id/flutter/flutter_background_service/BootReceiver.java#L20
+const val SERVICE_WAKELOCK_DURATION = 10 * 60 * 1000L
+const val SERVICE_DEFAULT_TITLE = "Moxxy"
+const val SERVICE_DEFAULT_BODY = "Preparing..."
+const val SERVICE_BACKGROUND_METHOD_CHANNEL_KEY = "org.moxxy.moxxy_native/background"
diff --git a/android/src/main/kotlin/org/moxxy/moxxy_native/MoxxyNativePlugin.kt b/android/src/main/kotlin/org/moxxy/moxxy_native/MoxxyNativePlugin.kt
index 5b1e632..eb517bb 100644
--- a/android/src/main/kotlin/org/moxxy/moxxy_native/MoxxyNativePlugin.kt
+++ b/android/src/main/kotlin/org/moxxy/moxxy_native/MoxxyNativePlugin.kt
@@ -10,6 +10,8 @@ import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
+import io.flutter.embedding.engine.plugins.service.ServiceAware
+import io.flutter.embedding.engine.plugins.service.ServicePluginBinding
import io.flutter.plugin.common.EventChannel
import org.moxxy.moxxy_native.contacts.ContactsImplementation
import org.moxxy.moxxy_native.contacts.MoxxyContactsApi
@@ -25,6 +27,10 @@ import org.moxxy.moxxy_native.picker.MoxxyPickerApi
import org.moxxy.moxxy_native.picker.PickerResultListener
import org.moxxy.moxxy_native.platform.MoxxyPlatformApi
import org.moxxy.moxxy_native.platform.PlatformImplementation
+import org.moxxy.moxxy_native.service.BackgroundService
+import org.moxxy.moxxy_native.service.MoxxyServiceApi
+import org.moxxy.moxxy_native.service.PluginTracker
+import org.moxxy.moxxy_native.service.ServiceImplementation
object MoxxyEventChannels {
var notificationChannel: EventChannel? = null
@@ -50,7 +56,7 @@ object NotificationCache {
var lastEvent: NotificationEvent? = null
}
-class MoxxyNativePlugin : FlutterPlugin, ActivityAware, MoxxyPickerApi {
+class MoxxyNativePlugin : FlutterPlugin, ActivityAware, ServiceAware, MoxxyPickerApi {
private var context: Context? = null
private var activity: Activity? = null
private lateinit var pickerListener: PickerResultListener
@@ -59,12 +65,20 @@ class MoxxyNativePlugin : FlutterPlugin, ActivityAware, MoxxyPickerApi {
private lateinit var platformImplementation: PlatformImplementation
private val mediaImplementation = MediaImplementation()
private lateinit var notificationsImplementation: NotificationsImplementation
+ private lateinit var serviceImplementation: ServiceImplementation
+
+ var service: BackgroundService? = null
+
+ init {
+ PluginTracker.instances.add(this)
+ }
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
contactsImplementation = ContactsImplementation(context!!)
platformImplementation = PlatformImplementation(context!!)
notificationsImplementation = NotificationsImplementation(context!!)
+ serviceImplementation = ServiceImplementation(context!!)
// Register the pigeon handlers
MoxxyPickerApi.setUp(flutterPluginBinding.binaryMessenger, this)
@@ -73,6 +87,7 @@ class MoxxyNativePlugin : FlutterPlugin, ActivityAware, MoxxyPickerApi {
MoxxyContactsApi.setUp(flutterPluginBinding.binaryMessenger, contactsImplementation)
MoxxyPlatformApi.setUp(flutterPluginBinding.binaryMessenger, platformImplementation)
MoxxyMediaApi.setUp(flutterPluginBinding.binaryMessenger, mediaImplementation)
+ MoxxyServiceApi.setUp(flutterPluginBinding.binaryMessenger, serviceImplementation)
// Register the picker handler
pickerListener = PickerResultListener(context!!)
@@ -103,6 +118,16 @@ class MoxxyNativePlugin : FlutterPlugin, ActivityAware, MoxxyPickerApi {
Log.d(TAG, "Detached from activity")
}
+ override fun onAttachedToService(binding: ServicePluginBinding) {
+ Log.d(TAG, "Attached to service")
+ service = binding.getService() as BackgroundService
+ }
+
+ override fun onDetachedFromService() {
+ Log.d(TAG, "Detached from service")
+ service = null
+ }
+
override fun pickFiles(
type: FilePickerType,
multiple: Boolean,
diff --git a/android/src/main/kotlin/org/moxxy/moxxy_native/service/BackgroundService.kt b/android/src/main/kotlin/org/moxxy/moxxy_native/service/BackgroundService.kt
new file mode 100644
index 0000000..c0627f5
--- /dev/null
+++ b/android/src/main/kotlin/org/moxxy/moxxy_native/service/BackgroundService.kt
@@ -0,0 +1,280 @@
+package org.moxxy.moxxy_native.service
+
+import android.app.AlarmManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.IBinder
+import android.os.PowerManager
+import android.os.PowerManager.WakeLock
+import android.util.Log
+import androidx.core.app.AlarmManagerCompat
+import androidx.core.app.NotificationCompat
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import io.flutter.FlutterInjector
+import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.embedding.engine.dart.DartExecutor
+import io.flutter.plugin.common.MethodChannel
+import io.flutter.view.FlutterCallbackInformation
+import org.moxxy.moxxy_native.R
+import org.moxxy.moxxy_native.SERVICE_BACKGROUND_METHOD_CHANNEL_KEY
+import org.moxxy.moxxy_native.SERVICE_DEFAULT_BODY
+import org.moxxy.moxxy_native.SERVICE_DEFAULT_TITLE
+import org.moxxy.moxxy_native.SERVICE_ENTRYPOINT_KEY
+import org.moxxy.moxxy_native.SERVICE_EXTRA_DATA_KEY
+import org.moxxy.moxxy_native.SERVICE_MANUALLY_STOPPED_KEY
+import org.moxxy.moxxy_native.SERVICE_SHARED_PREFERENCES_KEY
+import org.moxxy.moxxy_native.SERVICE_START_AT_BOOT_KEY
+import org.moxxy.moxxy_native.SERVICE_WAKELOCK_DURATION
+import org.moxxy.moxxy_native.TAG
+import org.moxxy.moxxy_native.service.background.MoxxyBackgroundServiceApi
+import java.util.concurrent.atomic.AtomicBoolean
+
+object BackgroundServiceStatic {
+ @Volatile
+ var wakeLock: WakeLock? = null
+
+ fun acquireWakeLock(context: Context): WakeLock {
+ if (wakeLock == null) {
+ val manager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
+ wakeLock =
+ manager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "${this.javaClass.name}.class")
+ wakeLock!!.setReferenceCounted(true)
+ }
+
+ return wakeLock!!
+ }
+
+ fun enqueue(context: Context) {
+ val mutable =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0
+ val pendingIntent = PendingIntent.getBroadcast(
+ context,
+ 111,
+ Intent(context, WatchdogReceiver::class.java),
+ PendingIntent.FLAG_UPDATE_CURRENT or mutable,
+ )
+
+ AlarmManagerCompat.setAndAllowWhileIdle(
+ context.getSystemService(Context.ALARM_SERVICE) as AlarmManager,
+ AlarmManager.RTC_WAKEUP,
+ System.currentTimeMillis() + 5000,
+ pendingIntent,
+ )
+ }
+
+ fun getStartAtBoot(context: Context): Boolean {
+ return context.getSharedPreferences(SERVICE_SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
+ .getBoolean(
+ SERVICE_START_AT_BOOT_KEY,
+ false,
+ )
+ }
+
+ fun setStartAtBoot(context: Context, value: Boolean) {
+ context.getSharedPreferences(SERVICE_SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).edit()
+ .putBoolean(SERVICE_START_AT_BOOT_KEY, value)
+ .apply()
+ }
+
+ fun getManuallyStopped(context: Context): Boolean {
+ return context.getSharedPreferences(SERVICE_SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
+ .getBoolean(
+ SERVICE_MANUALLY_STOPPED_KEY,
+ false,
+ )
+ }
+}
+
+class BackgroundService : Service(), MoxxyBackgroundServiceApi {
+
+ // Indicates whether the background service is running or not
+ private var isRunning = AtomicBoolean(false)
+
+ // Indicates whether the service was stopped manually
+ private var isManuallyStopped = false
+
+ // If non-null, the Flutter Engine that is running the background service's code
+ private var engine: FlutterEngine? = null
+
+ // The callback for Dart to start execution at
+ private var dartCallback: DartExecutor.DartCallback? = null
+
+ // Method channel for Java -> Dart
+ private var methodChannel: MethodChannel? = null
+
+ // Data for the notification
+ private var notificationTitle: String = SERVICE_DEFAULT_TITLE
+ private var notificationBody: String = SERVICE_DEFAULT_BODY
+
+ private fun setManuallyStopped(context: Context, value: Boolean) {
+ context.getSharedPreferences(SERVICE_SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).edit()
+ .putBoolean(SERVICE_MANUALLY_STOPPED_KEY, value)
+ .apply()
+ }
+
+ private fun getHandle(): Long {
+ return getSharedPreferences(SERVICE_SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).getLong(
+ SERVICE_ENTRYPOINT_KEY,
+ 0,
+ )
+ }
+
+ private fun updateNotificationInfo() {
+ val mutable =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0
+ val pendingIntent = PendingIntent.getActivity(
+ this,
+ 99778,
+ packageManager.getLaunchIntentForPackage(applicationContext.packageName),
+ PendingIntent.FLAG_CANCEL_CURRENT or mutable,
+ )
+
+ val notification = NotificationCompat.Builder(this, "foreground_service").apply {
+ setSmallIcon(R.drawable.ic_service)
+ setAutoCancel(false)
+ setOngoing(true)
+ setContentTitle(notificationTitle)
+ setContentText(notificationBody)
+ setContentIntent(pendingIntent)
+ }.build()
+ startForeground(99778, notification)
+ }
+
+ private fun runService() {
+ try {
+ if (isRunning.get() || (engine?.getDartExecutor()?.isExecutingDart() ?: false)) return
+
+ if (BackgroundServiceStatic.wakeLock == null) {
+ Log.d(TAG, "WakeLock is null. Acquiring and grabbing WakeLock...")
+ BackgroundServiceStatic.acquireWakeLock(applicationContext)
+ .acquire(SERVICE_WAKELOCK_DURATION)
+ Log.d(TAG, "WakeLock grabbed")
+ }
+
+ // Update the notification
+ updateNotificationInfo()
+
+ // Set-up the Flutter Engine, if it's not already set up
+ if (!FlutterInjector.instance().flutterLoader().initialized()) {
+ FlutterInjector.instance().flutterLoader().startInitialization(applicationContext)
+ }
+ FlutterInjector.instance().flutterLoader().ensureInitializationComplete(
+ applicationContext,
+ null,
+ )
+ val callback: FlutterCallbackInformation =
+ FlutterCallbackInformation.lookupCallbackInformation(getHandle())
+ if (callback == null) {
+ Log.e(TAG, "Callback handle not found")
+ return
+ }
+ isRunning.set(true)
+ engine = FlutterEngine(this)
+ engine!!.getServiceControlSurface().attachToService(this, null, true)
+ methodChannel = MethodChannel(
+ engine!!.getDartExecutor()!!.getBinaryMessenger(),
+ SERVICE_BACKGROUND_METHOD_CHANNEL_KEY,
+ )
+
+ MoxxyBackgroundServiceApi.setUp(engine!!.getDartExecutor()!!.getBinaryMessenger(), this)
+ Log.d(TAG, "MoxxyBackgroundServiceApi ready")
+
+ dartCallback = DartExecutor.DartCallback(
+ assets,
+ FlutterInjector.instance().flutterLoader().findAppBundlePath(),
+ callback,
+ )
+ engine!!.getDartExecutor().executeDartCallback(dartCallback!!)
+ } catch (ex: UnsatisfiedLinkError) {
+ Log.e(TAG, "Failed to set up background service: ${ex.message}")
+ }
+ }
+
+ override fun onBind(intent: Intent): IBinder? {
+ return null
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+
+ notificationBody = SERVICE_DEFAULT_BODY
+ updateNotificationInfo()
+ }
+
+ override fun onDestroy() {
+ if (!isManuallyStopped) {
+ BackgroundServiceStatic.enqueue(this)
+ } else {
+ setManuallyStopped(applicationContext, true)
+ }
+
+ // Dispose of the engine
+ engine?.apply {
+ getServiceControlSurface().detachFromService()
+ destroy()
+ }
+ engine = null
+ dartCallback = null
+
+ // Stop the service
+ stopForeground(true)
+ isRunning.set(false)
+
+ super.onDestroy()
+ }
+
+ fun receiveData(data: String) {
+ methodChannel?.invokeMethod("dataReceived", data)
+ }
+
+ override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
+ setManuallyStopped(this, false)
+ BackgroundServiceStatic.enqueue(this)
+ runService()
+
+ return START_STICKY
+ }
+
+ override fun getHandler(): Long {
+ return getHandle()
+ }
+
+ override fun getExtraData(): String {
+ return getSharedPreferences(SERVICE_SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).getString(
+ SERVICE_EXTRA_DATA_KEY,
+ "",
+ )!!
+ }
+
+ override fun setNotificationBody(body: String) {
+ notificationBody = body
+ updateNotificationInfo()
+ }
+
+ override fun sendData(data: String) {
+ LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(
+ Intent(SERVICE_BACKGROUND_METHOD_CHANNEL_KEY).apply {
+ putExtra("data", data)
+ },
+ )
+ }
+
+ override fun stop() {
+ isManuallyStopped = true
+ val mutable =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0
+ val pendingIntent = PendingIntent.getBroadcast(
+ applicationContext,
+ 111,
+ Intent(this, WatchdogReceiver::class.java),
+ PendingIntent.FLAG_CANCEL_CURRENT or mutable,
+ )
+ val stopManager = getSystemService(ALARM_SERVICE) as AlarmManager
+ stopManager.cancel(pendingIntent)
+ stopSelf()
+ BackgroundServiceStatic.setStartAtBoot(applicationContext, false)
+ }
+}
diff --git a/android/src/main/kotlin/org/moxxy/moxxy_native/service/BootReceiver.kt b/android/src/main/kotlin/org/moxxy/moxxy_native/service/BootReceiver.kt
new file mode 100644
index 0000000..87d9a7b
--- /dev/null
+++ b/android/src/main/kotlin/org/moxxy/moxxy_native/service/BootReceiver.kt
@@ -0,0 +1,25 @@
+package org.moxxy.moxxy_native.service
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import androidx.core.content.ContextCompat
+import org.moxxy.moxxy_native.TAG
+
+class BootReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (BackgroundServiceStatic.getStartAtBoot(context)) {
+ if (BackgroundServiceStatic.wakeLock == null) {
+ Log.d(TAG, "WakeLock is null. Acquiring it...")
+ BackgroundServiceStatic.acquireWakeLock(context)
+ Log.d(TAG, "WakeLock acquired")
+ }
+
+ ContextCompat.startForegroundService(
+ context,
+ Intent(context, BackgroundService::class.java),
+ )
+ }
+ }
+}
diff --git a/android/src/main/kotlin/org/moxxy/moxxy_native/service/ServiceApi.kt b/android/src/main/kotlin/org/moxxy/moxxy_native/service/ServiceApi.kt
new file mode 100644
index 0000000..e158feb
--- /dev/null
+++ b/android/src/main/kotlin/org/moxxy/moxxy_native/service/ServiceApi.kt
@@ -0,0 +1,134 @@
+// Autogenerated from Pigeon (v11.0.1), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+
+package org.moxxy.moxxy_native.service
+
+import android.util.Log
+import io.flutter.plugin.common.BasicMessageChannel
+import io.flutter.plugin.common.BinaryMessenger
+import io.flutter.plugin.common.MessageCodec
+import io.flutter.plugin.common.StandardMessageCodec
+
+private fun wrapResult(result: Any?): List {
+ return listOf(result)
+}
+
+private fun wrapError(exception: Throwable): List {
+ if (exception is FlutterError) {
+ return listOf(
+ exception.code,
+ exception.message,
+ exception.details,
+ )
+ } else {
+ return listOf(
+ exception.javaClass.simpleName,
+ exception.toString(),
+ "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception),
+ )
+ }
+}
+
+/**
+ * Error class for passing custom error details to Flutter via a thrown PlatformException.
+ * @property code The error code.
+ * @property message The error message.
+ * @property details The error details. Must be a datatype supported by the api codec.
+ */
+class FlutterError(
+ val code: String,
+ override val message: String? = null,
+ val details: Any? = null,
+) : Throwable()
+
+/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
+interface MoxxyServiceApi {
+ fun configure(handle: Long, extraData: String)
+ fun isRunning(): Boolean
+ fun start()
+ fun sendData(data: String)
+
+ companion object {
+ /** The codec used by MoxxyServiceApi. */
+ val codec: MessageCodec by lazy {
+ StandardMessageCodec()
+ }
+
+ /** Sets up an instance of `MoxxyServiceApi` to handle messages through the `binaryMessenger`. */
+ @Suppress("UNCHECKED_CAST")
+ fun setUp(binaryMessenger: BinaryMessenger, api: MoxxyServiceApi?) {
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyServiceApi.configure", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val handleArg = args[0].let { if (it is Int) it.toLong() else it as Long }
+ val extraDataArg = args[1] as String
+ var wrapped: List
+ try {
+ api.configure(handleArg, extraDataArg)
+ wrapped = listOf(null)
+ } catch (exception: Throwable) {
+ wrapped = wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyServiceApi.isRunning", codec)
+ if (api != null) {
+ channel.setMessageHandler { _, reply ->
+ var wrapped: List
+ try {
+ wrapped = listOf(api.isRunning())
+ } catch (exception: Throwable) {
+ wrapped = wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyServiceApi.start", codec)
+ if (api != null) {
+ channel.setMessageHandler { _, reply ->
+ var wrapped: List
+ try {
+ api.start()
+ wrapped = listOf(null)
+ } catch (exception: Throwable) {
+ wrapped = wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyServiceApi.sendData", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val dataArg = args[0] as String
+ var wrapped: List
+ try {
+ api.sendData(dataArg)
+ wrapped = listOf(null)
+ } catch (exception: Throwable) {
+ wrapped = wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ }
+ }
+}
diff --git a/android/src/main/kotlin/org/moxxy/moxxy_native/service/ServiceImplementation.kt b/android/src/main/kotlin/org/moxxy/moxxy_native/service/ServiceImplementation.kt
new file mode 100644
index 0000000..77f8891
--- /dev/null
+++ b/android/src/main/kotlin/org/moxxy/moxxy_native/service/ServiceImplementation.kt
@@ -0,0 +1,57 @@
+package org.moxxy.moxxy_native.service
+
+import android.app.ActivityManager
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import androidx.core.content.ContextCompat
+import org.moxxy.moxxy_native.MoxxyNativePlugin
+import org.moxxy.moxxy_native.SERVICE_ENTRYPOINT_KEY
+import org.moxxy.moxxy_native.SERVICE_EXTRA_DATA_KEY
+import org.moxxy.moxxy_native.SERVICE_SHARED_PREFERENCES_KEY
+import org.moxxy.moxxy_native.TAG
+import org.moxxy.moxxy_native.service.BackgroundServiceStatic.setStartAtBoot
+
+object PluginTracker {
+ var instances: MutableList = mutableListOf()
+}
+
+class ServiceImplementation(private val context: Context) : MoxxyServiceApi {
+ override fun configure(handle: Long, extraData: String) {
+ context.getSharedPreferences(SERVICE_SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).edit()
+ .putLong(SERVICE_ENTRYPOINT_KEY, handle)
+ .putString(SERVICE_EXTRA_DATA_KEY, extraData)
+ .apply()
+ }
+
+ override fun isRunning(): Boolean {
+ val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+ for (info in manager.getRunningServices(Int.MAX_VALUE)) {
+ if (BackgroundService::class.java.name == info.service.className) {
+ return true
+ }
+ }
+
+ return false
+ }
+
+ override fun start() {
+ setStartAtBoot(context, true)
+ BackgroundServiceStatic.enqueue(context)
+ ContextCompat.startForegroundService(
+ context,
+ Intent(context, BackgroundService::class.java),
+ )
+ Log.d(TAG, "Background service started")
+ }
+
+ override fun sendData(data: String) {
+ for (plugin in PluginTracker.instances) {
+ val service = plugin.service
+ if (service != null) {
+ service.receiveData(data)
+ break
+ }
+ }
+ }
+}
diff --git a/android/src/main/kotlin/org/moxxy/moxxy_native/service/WatchdogReceiver.kt b/android/src/main/kotlin/org/moxxy/moxxy_native/service/WatchdogReceiver.kt
new file mode 100644
index 0000000..4816157
--- /dev/null
+++ b/android/src/main/kotlin/org/moxxy/moxxy_native/service/WatchdogReceiver.kt
@@ -0,0 +1,18 @@
+package org.moxxy.moxxy_native.service
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import androidx.core.content.ContextCompat
+import org.moxxy.moxxy_native.service.BackgroundServiceStatic.getManuallyStopped
+
+class WatchdogReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent?) {
+ if (!getManuallyStopped(context)) {
+ ContextCompat.startForegroundService(
+ context,
+ Intent(context, BackgroundService::class.java),
+ )
+ }
+ }
+}
diff --git a/android/src/main/kotlin/org/moxxy/moxxy_native/service/background/BackgroundServiceApi.kt b/android/src/main/kotlin/org/moxxy/moxxy_native/service/background/BackgroundServiceApi.kt
new file mode 100644
index 0000000..c3d9a61
--- /dev/null
+++ b/android/src/main/kotlin/org/moxxy/moxxy_native/service/background/BackgroundServiceApi.kt
@@ -0,0 +1,150 @@
+// Autogenerated from Pigeon (v11.0.1), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+
+package org.moxxy.moxxy_native.service.background
+
+import android.util.Log
+import io.flutter.plugin.common.BasicMessageChannel
+import io.flutter.plugin.common.BinaryMessenger
+import io.flutter.plugin.common.MessageCodec
+import io.flutter.plugin.common.StandardMessageCodec
+
+private fun wrapResult(result: Any?): List {
+ return listOf(result)
+}
+
+private fun wrapError(exception: Throwable): List {
+ if (exception is FlutterError) {
+ return listOf(
+ exception.code,
+ exception.message,
+ exception.details,
+ )
+ } else {
+ return listOf(
+ exception.javaClass.simpleName,
+ exception.toString(),
+ "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception),
+ )
+ }
+}
+
+/**
+ * Error class for passing custom error details to Flutter via a thrown PlatformException.
+ * @property code The error code.
+ * @property message The error message.
+ * @property details The error details. Must be a datatype supported by the api codec.
+ */
+class FlutterError(
+ val code: String,
+ override val message: String? = null,
+ val details: Any? = null,
+) : Throwable()
+
+/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
+interface MoxxyBackgroundServiceApi {
+ fun getHandler(): Long
+ fun getExtraData(): String
+ fun setNotificationBody(body: String)
+ fun sendData(data: String)
+ fun stop()
+
+ companion object {
+ /** The codec used by MoxxyBackgroundServiceApi. */
+ val codec: MessageCodec by lazy {
+ StandardMessageCodec()
+ }
+
+ /** Sets up an instance of `MoxxyBackgroundServiceApi` to handle messages through the `binaryMessenger`. */
+ @Suppress("UNCHECKED_CAST")
+ fun setUp(binaryMessenger: BinaryMessenger, api: MoxxyBackgroundServiceApi?) {
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.getHandler", codec)
+ if (api != null) {
+ channel.setMessageHandler { _, reply ->
+ var wrapped: List
+ try {
+ wrapped = listOf(api.getHandler())
+ } catch (exception: Throwable) {
+ wrapped = wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.getExtraData", codec)
+ if (api != null) {
+ channel.setMessageHandler { _, reply ->
+ var wrapped: List
+ try {
+ wrapped = listOf(api.getExtraData())
+ } catch (exception: Throwable) {
+ wrapped = wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.setNotificationBody", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val bodyArg = args[0] as String
+ var wrapped: List
+ try {
+ api.setNotificationBody(bodyArg)
+ wrapped = listOf(null)
+ } catch (exception: Throwable) {
+ wrapped = wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.sendData", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val dataArg = args[0] as String
+ var wrapped: List
+ try {
+ api.sendData(dataArg)
+ wrapped = listOf(null)
+ } catch (exception: Throwable) {
+ wrapped = wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.stop", codec)
+ if (api != null) {
+ channel.setMessageHandler { _, reply ->
+ var wrapped: List
+ try {
+ api.stop()
+ wrapped = listOf(null)
+ } catch (exception: Throwable) {
+ wrapped = wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ }
+ }
+}
diff --git a/example/lib/main.dart b/example/lib/main.dart
index 5ad8582..95d08a4 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -1,7 +1,18 @@
import 'dart:io';
import 'dart:typed_data';
+import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:moxxy_native/moxxy_native.dart';
+import 'package:permission_handler/permission_handler.dart';
+
+@pragma('vm:entry-point')
+Future entrypoint() async {
+ WidgetsFlutterBinding.ensureInitialized();
+
+ print('CALLED FROM NEW FLUTTERENGINE');
+ final extra = await MoxxyBackgroundServiceApi().getExtraData();
+ print('EXTRA DATA: $extra');
+}
void main() {
runApp(const MyApp());
@@ -96,6 +107,32 @@ class MyAppState extends State {
child: const Text('Test cryptography'),
),
if (imagePath != null) Image.file(File(imagePath!)),
+ TextButton(
+ onPressed: () async {
+ // Create channel
+ await MoxxyNotificationsApi().createNotificationChannels(
+ [
+ NotificationChannel(
+ id: 'foreground_service',
+ title: 'Foreground service',
+ description: 'lol',
+ importance: NotificationChannelImportance.MIN,
+ showBadge: false,
+ vibration: false,
+ enableLights: false,
+ ),
+ ],
+ );
+
+ await Permission.notification.request();
+
+ final handle = PluginUtilities.getCallbackHandle(entrypoint)!
+ .toRawHandle();
+ final api = MoxxyServiceApi();
+ await api.configure(handle, 'lol');
+ await api.start();
+ },
+ child: const Text('Start foreground service')),
],
),
),
diff --git a/example/pubspec.lock b/example/pubspec.lock
index 14fd041..9d7235d 100644
--- a/example/pubspec.lock
+++ b/example/pubspec.lock
@@ -130,6 +130,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.8.2"
+ permission_handler:
+ dependency: "direct main"
+ description:
+ name: permission_handler
+ sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5
+ url: "https://pub.dev"
+ source: hosted
+ version: "10.4.5"
+ permission_handler_android:
+ dependency: transitive
+ description:
+ name: permission_handler_android
+ sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47"
+ url: "https://pub.dev"
+ source: hosted
+ version: "10.3.6"
+ permission_handler_apple:
+ dependency: transitive
+ description:
+ name: permission_handler_apple
+ sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5"
+ url: "https://pub.dev"
+ source: hosted
+ version: "9.1.4"
+ permission_handler_platform_interface:
+ dependency: transitive
+ description:
+ name: permission_handler_platform_interface
+ sha256: f2343e9fa9c22ae4fd92d4732755bfe452214e7189afcc097380950cf567b4b2
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.11.5"
+ permission_handler_windows:
+ dependency: transitive
+ description:
+ name: permission_handler_windows
+ sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.1.3"
+ plugin_platform_interface:
+ dependency: transitive
+ description:
+ name: plugin_platform_interface
+ sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.6"
sky_engine:
dependency: transitive
description: flutter
@@ -193,4 +241,4 @@ packages:
version: "2.1.4"
sdks:
dart: ">=2.19.6 <3.0.0"
- flutter: ">=2.5.0"
+ flutter: ">=2.8.0"
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index 4ccfa64..3ec4c28 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -28,6 +28,7 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
+ permission_handler: ^10.4.5
dev_dependencies:
flutter_test:
diff --git a/flake.nix b/flake.nix
index eaaac6b..87bdce1 100644
--- a/flake.nix
+++ b/flake.nix
@@ -66,5 +66,19 @@
# an used parameter.
GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${sdk}/share/android-sdk/build-tools/34.0.0/aapt2";
};
+
+ apps = {
+ androidLint = let
+ script = pkgs.writeShellScript "lint-android.sh" ''
+ ${pkgs.ktlint}/bin/ktlint \
+ --format \
+ --disabled_rules=standard:package-name \
+ android/src/main/kotlin/org/moxxy/moxxy_native/
+ '';
+ in {
+ program = "${script}";
+ type = "app";
+ };
+ };
});
}
diff --git a/lib/moxxy_native.dart b/lib/moxxy_native.dart
index 9e985da..1b8f344 100644
--- a/lib/moxxy_native.dart
+++ b/lib/moxxy_native.dart
@@ -1,6 +1,8 @@
+export 'pigeon/background_service.g.dart';
export 'pigeon/contacts.g.dart';
export 'pigeon/cryptography.g.dart';
export 'pigeon/media.g.dart';
export 'pigeon/notifications.g.dart';
export 'pigeon/picker.g.dart';
export 'pigeon/platform.g.dart';
+export 'pigeon/service.g.dart';
diff --git a/lib/pigeon/background_service.g.dart b/lib/pigeon/background_service.g.dart
new file mode 100644
index 0000000..37a0868
--- /dev/null
+++ b/lib/pigeon/background_service.g.dart
@@ -0,0 +1,140 @@
+// Autogenerated from Pigeon (v11.0.1), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import
+
+import 'dart:async';
+import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
+
+import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
+import 'package:flutter/services.dart';
+
+class MoxxyBackgroundServiceApi {
+ /// Constructor for [MoxxyBackgroundServiceApi]. The [binaryMessenger] named argument is
+ /// available for dependency injection. If it is left null, the default
+ /// BinaryMessenger will be used which routes to the host platform.
+ MoxxyBackgroundServiceApi({BinaryMessenger? binaryMessenger})
+ : _binaryMessenger = binaryMessenger;
+ final BinaryMessenger? _binaryMessenger;
+
+ static const MessageCodec