From dfbb64c8ae12a8cc8952e448fac85cc2458e5395 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sat, 9 Sep 2023 00:28:01 +0200 Subject: [PATCH] feat: Move over the service/background service API --- android/build.gradle | 1 + android/src/main/AndroidManifest.xml | 30 +- .../org/moxxy/moxxy_native/Constants.kt | 13 + .../moxxy/moxxy_native/MoxxyNativePlugin.kt | 27 +- .../moxxy_native/service/BackgroundService.kt | 280 ++++++++++++++++++ .../moxxy_native/service/BootReceiver.kt | 25 ++ .../moxxy/moxxy_native/service/ServiceApi.kt | 134 +++++++++ .../service/ServiceImplementation.kt | 57 ++++ .../moxxy_native/service/WatchdogReceiver.kt | 18 ++ .../background/BackgroundServiceApi.kt | 150 ++++++++++ example/lib/main.dart | 37 +++ example/pubspec.lock | 50 +++- example/pubspec.yaml | 1 + flake.nix | 14 + lib/moxxy_native.dart | 2 + lib/pigeon/background_service.g.dart | 140 +++++++++ lib/pigeon/service.g.dart | 113 +++++++ pigeon/background_service.dart | 25 ++ pigeon/service.dart | 23 ++ 19 files changed, 1137 insertions(+), 3 deletions(-) create mode 100644 android/src/main/kotlin/org/moxxy/moxxy_native/service/BackgroundService.kt create mode 100644 android/src/main/kotlin/org/moxxy/moxxy_native/service/BootReceiver.kt create mode 100644 android/src/main/kotlin/org/moxxy/moxxy_native/service/ServiceApi.kt create mode 100644 android/src/main/kotlin/org/moxxy/moxxy_native/service/ServiceImplementation.kt create mode 100644 android/src/main/kotlin/org/moxxy/moxxy_native/service/WatchdogReceiver.kt create mode 100644 android/src/main/kotlin/org/moxxy/moxxy_native/service/background/BackgroundServiceApi.kt create mode 100644 lib/pigeon/background_service.g.dart create mode 100644 lib/pigeon/service.g.dart create mode 100644 pigeon/background_service.dart create mode 100644 pigeon/service.dart 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 codec = StandardMessageCodec(); + + Future getHandler() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.getHandler', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as int?)!; + } + } + + Future getExtraData() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.getExtraData', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as String?)!; + } + } + + Future setNotificationBody(String arg_body) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.setNotificationBody', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_body]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future sendData(String arg_data) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.sendData', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_data]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future stop() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.stop', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } +} diff --git a/lib/pigeon/service.g.dart b/lib/pigeon/service.g.dart new file mode 100644 index 0000000..caaf54a --- /dev/null +++ b/lib/pigeon/service.g.dart @@ -0,0 +1,113 @@ +// 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 MoxxyServiceApi { + /// Constructor for [MoxxyServiceApi]. 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. + MoxxyServiceApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = StandardMessageCodec(); + + Future configure(int arg_handle, String arg_extraData) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.moxxy_native.MoxxyServiceApi.configure', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_handle, arg_extraData]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future isRunning() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.moxxy_native.MoxxyServiceApi.isRunning', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as bool?)!; + } + } + + Future start() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.moxxy_native.MoxxyServiceApi.start', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future sendData(String arg_data) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.moxxy_native.MoxxyServiceApi.sendData', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_data]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } +} diff --git a/pigeon/background_service.dart b/pigeon/background_service.dart new file mode 100644 index 0000000..b13efaf --- /dev/null +++ b/pigeon/background_service.dart @@ -0,0 +1,25 @@ +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/pigeon/background_service.g.dart', + kotlinOut: + 'android/src/main/kotlin/org/moxxy/moxxy_native/service/background/BackgroundServiceApi.kt', + kotlinOptions: KotlinOptions( + package: 'org.moxxy.moxxy_native.service.background', + ), + ), +) + +@HostApi() +abstract class MoxxyBackgroundServiceApi { + int getHandler(); + + String getExtraData(); + + void setNotificationBody(String body); + + void sendData(String data); + + void stop(); +} diff --git a/pigeon/service.dart b/pigeon/service.dart new file mode 100644 index 0000000..1e7b933 --- /dev/null +++ b/pigeon/service.dart @@ -0,0 +1,23 @@ +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/pigeon/service.g.dart', + kotlinOut: + 'android/src/main/kotlin/org/moxxy/moxxy_native/service/ServiceApi.kt', + kotlinOptions: KotlinOptions( + package: 'org.moxxy.moxxy_native.service', + ), + ), +) + +@HostApi() +abstract class MoxxyServiceApi { + void configure(int handle, String extraData); + + bool isRunning(); + + void start(); + + void sendData(String data); +}