diff --git a/android/src/main/kotlin/org/moxxy/moxxy_native/notifications/NotificationsApi.kt b/android/src/main/kotlin/org/moxxy/moxxy_native/notifications/NotificationsApi.kt index 4a3272d..3e7765d 100644 --- a/android/src/main/kotlin/org/moxxy/moxxy_native/notifications/NotificationsApi.kt +++ b/android/src/main/kotlin/org/moxxy/moxxy_native/notifications/NotificationsApi.kt @@ -12,23 +12,23 @@ import java.io.ByteArrayOutputStream import java.nio.ByteBuffer private fun wrapResult(result: Any?): List { - return listOf(result) + 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), - ) - } + 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) + ) + } } /** @@ -37,635 +37,630 @@ private fun wrapError(exception: Throwable): List { * @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, +class FlutterError ( + val code: String, + override val message: String? = null, + val details: Any? = null ) : Throwable() enum class NotificationIcon(val raw: Int) { - WARNING(0), - ERROR(1), - NONE(2), - ; + WARNING(0), + ERROR(1), + NONE(2); - companion object { - fun ofRaw(raw: Int): NotificationIcon? { - return values().firstOrNull { it.raw == raw } - } + companion object { + fun ofRaw(raw: Int): NotificationIcon? { + return values().firstOrNull { it.raw == raw } } + } } enum class NotificationEventType(val raw: Int) { - MARKASREAD(0), - REPLY(1), - OPEN(2), - ; + MARKASREAD(0), + REPLY(1), + OPEN(2); - companion object { - fun ofRaw(raw: Int): NotificationEventType? { - return values().firstOrNull { it.raw == raw } - } + companion object { + fun ofRaw(raw: Int): NotificationEventType? { + return values().firstOrNull { it.raw == raw } } + } } enum class NotificationChannelImportance(val raw: Int) { - MIN(0), - HIGH(1), - DEFAULT(2), - ; + MIN(0), + HIGH(1), + DEFAULT(2); - companion object { - fun ofRaw(raw: Int): NotificationChannelImportance? { - return values().firstOrNull { it.raw == raw } - } + companion object { + fun ofRaw(raw: Int): NotificationChannelImportance? { + return values().firstOrNull { it.raw == raw } } + } } /** Generated class from Pigeon that represents data sent in messages. */ -data class NotificationMessageContent( - /** The textual body of the message. */ - val body: String? = null, - /** The path and mime type of the media to show. */ - val mime: String? = null, - val path: String? = null, +data class NotificationMessageContent ( + /** The textual body of the message. */ + val body: String? = null, + /** The path and mime type of the media to show. */ + val mime: String? = null, + val path: String? = null ) { - companion object { - @Suppress("UNCHECKED_CAST") - fun fromList(list: List): NotificationMessageContent { - val body = list[0] as String? - val mime = list[1] as String? - val path = list[2] as String? - return NotificationMessageContent(body, mime, path) - } - } - fun toList(): List { - return listOf( - body, - mime, - path, - ) + companion object { + @Suppress("UNCHECKED_CAST") + fun fromList(list: List): NotificationMessageContent { + val body = list[0] as String? + val mime = list[1] as String? + val path = list[2] as String? + return NotificationMessageContent(body, mime, path) } + } + fun toList(): List { + return listOf( + body, + mime, + path, + ) + } } /** Generated class from Pigeon that represents data sent in messages. */ -data class NotificationMessage( - /** The grouping key for the notification. */ - val groupId: String? = null, - /** The sender of the message. */ - val sender: String? = null, - /** The jid of the sender. */ - val jid: String? = null, - /** The body of the message. */ - val content: NotificationMessageContent, - /** Milliseconds since epoch. */ - val timestamp: Long, - /** The path to the avatar to use */ - val avatarPath: String? = null, +data class NotificationMessage ( + /** The grouping key for the notification. */ + val groupId: String? = null, + /** The sender of the message. */ + val sender: String? = null, + /** The jid of the sender. */ + val jid: String? = null, + /** The body of the message. */ + val content: NotificationMessageContent, + /** Milliseconds since epoch. */ + val timestamp: Long, + /** The path to the avatar to use */ + val avatarPath: String? = null ) { - companion object { - @Suppress("UNCHECKED_CAST") - fun fromList(list: List): NotificationMessage { - val groupId = list[0] as String? - val sender = list[1] as String? - val jid = list[2] as String? - val content = NotificationMessageContent.fromList(list[3] as List) - val timestamp = list[4].let { if (it is Int) it.toLong() else it as Long } - val avatarPath = list[5] as String? - return NotificationMessage(groupId, sender, jid, content, timestamp, avatarPath) - } - } - fun toList(): List { - return listOf( - groupId, - sender, - jid, - content.toList(), - timestamp, - avatarPath, - ) + companion object { + @Suppress("UNCHECKED_CAST") + fun fromList(list: List): NotificationMessage { + val groupId = list[0] as String? + val sender = list[1] as String? + val jid = list[2] as String? + val content = NotificationMessageContent.fromList(list[3] as List) + val timestamp = list[4].let { if (it is Int) it.toLong() else it as Long } + val avatarPath = list[5] as String? + return NotificationMessage(groupId, sender, jid, content, timestamp, avatarPath) } + } + fun toList(): List { + return listOf( + groupId, + sender, + jid, + content.toList(), + timestamp, + avatarPath, + ) + } } /** Generated class from Pigeon that represents data sent in messages. */ -data class MessagingNotification( - /** The title of the conversation. */ - val title: String, - /** The id of the notification. */ - val id: Long, - /** The id of the notification channel the notification should appear on. */ - val channelId: String, - /** The JID of the chat in which the notifications happen. */ - val jid: String, - /** Messages to show. */ - val messages: List, - /** Flag indicating whether this notification is from a groupchat or not. */ - val isGroupchat: Boolean, - /** The id for notification grouping. */ - val groupId: String? = null, - /** Additional data to include. */ - val extra: Map? = null, +data class MessagingNotification ( + /** The title of the conversation. */ + val title: String, + /** The id of the notification. */ + val id: Long, + /** The id of the notification channel the notification should appear on. */ + val channelId: String, + /** The JID of the chat in which the notifications happen. */ + val jid: String, + /** Messages to show. */ + val messages: List, + /** Flag indicating whether this notification is from a groupchat or not. */ + val isGroupchat: Boolean, + /** The id for notification grouping. */ + val groupId: String? = null, + /** Additional data to include. */ + val extra: Map? = null ) { - companion object { - @Suppress("UNCHECKED_CAST") - fun fromList(list: List): MessagingNotification { - val title = list[0] as String - val id = list[1].let { if (it is Int) it.toLong() else it as Long } - val channelId = list[2] as String - val jid = list[3] as String - val messages = list[4] as List - val isGroupchat = list[5] as Boolean - val groupId = list[6] as String? - val extra = list[7] as Map? - return MessagingNotification(title, id, channelId, jid, messages, isGroupchat, groupId, extra) - } - } - fun toList(): List { - return listOf( - title, - id, - channelId, - jid, - messages, - isGroupchat, - groupId, - extra, - ) + companion object { + @Suppress("UNCHECKED_CAST") + fun fromList(list: List): MessagingNotification { + val title = list[0] as String + val id = list[1].let { if (it is Int) it.toLong() else it as Long } + val channelId = list[2] as String + val jid = list[3] as String + val messages = list[4] as List + val isGroupchat = list[5] as Boolean + val groupId = list[6] as String? + val extra = list[7] as Map? + return MessagingNotification(title, id, channelId, jid, messages, isGroupchat, groupId, extra) } + } + fun toList(): List { + return listOf( + title, + id, + channelId, + jid, + messages, + isGroupchat, + groupId, + extra, + ) + } } /** Generated class from Pigeon that represents data sent in messages. */ -data class RegularNotification( - /** The title of the notification. */ - val title: String, - /** The body of the notification. */ - val body: String, - /** The id of the channel to show the notification on. */ - val channelId: String, - /** The id for notification grouping. */ - val groupId: String? = null, - /** The id of the notification. */ - val id: Long, - /** The icon to use. */ - val icon: NotificationIcon, +data class RegularNotification ( + /** The title of the notification. */ + val title: String, + /** The body of the notification. */ + val body: String, + /** The id of the channel to show the notification on. */ + val channelId: String, + /** The id for notification grouping. */ + val groupId: String? = null, + /** The id of the notification. */ + val id: Long, + /** The icon to use. */ + val icon: NotificationIcon ) { - companion object { - @Suppress("UNCHECKED_CAST") - fun fromList(list: List): RegularNotification { - val title = list[0] as String - val body = list[1] as String - val channelId = list[2] as String - val groupId = list[3] as String? - val id = list[4].let { if (it is Int) it.toLong() else it as Long } - val icon = NotificationIcon.ofRaw(list[5] as Int)!! - return RegularNotification(title, body, channelId, groupId, id, icon) - } - } - fun toList(): List { - return listOf( - title, - body, - channelId, - groupId, - id, - icon.raw, - ) + companion object { + @Suppress("UNCHECKED_CAST") + fun fromList(list: List): RegularNotification { + val title = list[0] as String + val body = list[1] as String + val channelId = list[2] as String + val groupId = list[3] as String? + val id = list[4].let { if (it is Int) it.toLong() else it as Long } + val icon = NotificationIcon.ofRaw(list[5] as Int)!! + return RegularNotification(title, body, channelId, groupId, id, icon) } + } + fun toList(): List { + return listOf( + title, + body, + channelId, + groupId, + id, + icon.raw, + ) + } } /** Generated class from Pigeon that represents data sent in messages. */ -data class NotificationEvent( - /** The notification id. */ - val id: Long, - /** The JID the notification was for. */ - val jid: String, - /** The type of event. */ - val type: NotificationEventType, - /** - * An optional payload. - * - type == NotificationType.reply: The reply message text. - * Otherwise: undefined. - */ - val payload: String? = null, - /** Extra data. Only set when type == NotificationType.reply. */ - val extra: Map? = null, +data class NotificationEvent ( + /** The notification id. */ + val id: Long, + /** The JID the notification was for. */ + val jid: String, + /** The type of event. */ + val type: NotificationEventType, + /** + * An optional payload. + * - type == NotificationType.reply: The reply message text. + * Otherwise: undefined. + */ + val payload: String? = null, + /** Extra data. Only set when type == NotificationType.reply. */ + val extra: Map? = null ) { - companion object { - @Suppress("UNCHECKED_CAST") - fun fromList(list: List): NotificationEvent { - val id = list[0].let { if (it is Int) it.toLong() else it as Long } - val jid = list[1] as String - val type = NotificationEventType.ofRaw(list[2] as Int)!! - val payload = list[3] as String? - val extra = list[4] as Map? - return NotificationEvent(id, jid, type, payload, extra) - } - } - fun toList(): List { - return listOf( - id, - jid, - type.raw, - payload, - extra, - ) + companion object { + @Suppress("UNCHECKED_CAST") + fun fromList(list: List): NotificationEvent { + val id = list[0].let { if (it is Int) it.toLong() else it as Long } + val jid = list[1] as String + val type = NotificationEventType.ofRaw(list[2] as Int)!! + val payload = list[3] as String? + val extra = list[4] as Map? + return NotificationEvent(id, jid, type, payload, extra) } + } + fun toList(): List { + return listOf( + id, + jid, + type.raw, + payload, + extra, + ) + } } /** Generated class from Pigeon that represents data sent in messages. */ -data class NotificationI18nData( - /** The content of the reply button. */ - val reply: String, - /** The content of the "mark as read" button. */ - val markAsRead: String, - /** The text to show when *you* reply. */ - val you: String, +data class NotificationI18nData ( + /** The content of the reply button. */ + val reply: String, + /** The content of the "mark as read" button. */ + val markAsRead: String, + /** The text to show when *you* reply. */ + val you: String ) { - companion object { - @Suppress("UNCHECKED_CAST") - fun fromList(list: List): NotificationI18nData { - val reply = list[0] as String - val markAsRead = list[1] as String - val you = list[2] as String - return NotificationI18nData(reply, markAsRead, you) - } - } - fun toList(): List { - return listOf( - reply, - markAsRead, - you, - ) + companion object { + @Suppress("UNCHECKED_CAST") + fun fromList(list: List): NotificationI18nData { + val reply = list[0] as String + val markAsRead = list[1] as String + val you = list[2] as String + return NotificationI18nData(reply, markAsRead, you) } + } + fun toList(): List { + return listOf( + reply, + markAsRead, + you, + ) + } } /** Generated class from Pigeon that represents data sent in messages. */ -data class NotificationGroup( - val id: String, - val description: String, +data class NotificationGroup ( + val id: String, + val description: String ) { - companion object { - @Suppress("UNCHECKED_CAST") - fun fromList(list: List): NotificationGroup { - val id = list[0] as String - val description = list[1] as String - return NotificationGroup(id, description) - } - } - fun toList(): List { - return listOf( - id, - description, - ) + companion object { + @Suppress("UNCHECKED_CAST") + fun fromList(list: List): NotificationGroup { + val id = list[0] as String + val description = list[1] as String + return NotificationGroup(id, description) } + } + fun toList(): List { + return listOf( + id, + description, + ) + } } /** Generated class from Pigeon that represents data sent in messages. */ -data class NotificationChannel( - val title: String, - val description: String, - val id: String, - val importance: NotificationChannelImportance, - val showBadge: Boolean, - val groupId: String? = null, - val vibration: Boolean, - val enableLights: Boolean, +data class NotificationChannel ( + val title: String, + val description: String, + val id: String, + val importance: NotificationChannelImportance, + val showBadge: Boolean, + val groupId: String? = null, + val vibration: Boolean, + val enableLights: Boolean ) { - companion object { - @Suppress("UNCHECKED_CAST") - fun fromList(list: List): NotificationChannel { - val title = list[0] as String - val description = list[1] as String - val id = list[2] as String - val importance = NotificationChannelImportance.ofRaw(list[3] as Int)!! - val showBadge = list[4] as Boolean - val groupId = list[5] as String? - val vibration = list[6] as Boolean - val enableLights = list[7] as Boolean - return NotificationChannel(title, description, id, importance, showBadge, groupId, vibration, enableLights) - } - } - fun toList(): List { - return listOf( - title, - description, - id, - importance.raw, - showBadge, - groupId, - vibration, - enableLights, - ) + companion object { + @Suppress("UNCHECKED_CAST") + fun fromList(list: List): NotificationChannel { + val title = list[0] as String + val description = list[1] as String + val id = list[2] as String + val importance = NotificationChannelImportance.ofRaw(list[3] as Int)!! + val showBadge = list[4] as Boolean + val groupId = list[5] as String? + val vibration = list[6] as Boolean + val enableLights = list[7] as Boolean + return NotificationChannel(title, description, id, importance, showBadge, groupId, vibration, enableLights) } + } + fun toList(): List { + return listOf( + title, + description, + id, + importance.raw, + showBadge, + groupId, + vibration, + enableLights, + ) + } } - @Suppress("UNCHECKED_CAST") private object MoxxyNotificationsApiCodec : StandardMessageCodec() { - override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { - return when (type) { - 128.toByte() -> { - return (readValue(buffer) as? List)?.let { - MessagingNotification.fromList(it) - } - } - 129.toByte() -> { - return (readValue(buffer) as? List)?.let { - NotificationChannel.fromList(it) - } - } - 130.toByte() -> { - return (readValue(buffer) as? List)?.let { - NotificationEvent.fromList(it) - } - } - 131.toByte() -> { - return (readValue(buffer) as? List)?.let { - NotificationGroup.fromList(it) - } - } - 132.toByte() -> { - return (readValue(buffer) as? List)?.let { - NotificationI18nData.fromList(it) - } - } - 133.toByte() -> { - return (readValue(buffer) as? List)?.let { - NotificationMessage.fromList(it) - } - } - 134.toByte() -> { - return (readValue(buffer) as? List)?.let { - NotificationMessageContent.fromList(it) - } - } - 135.toByte() -> { - return (readValue(buffer) as? List)?.let { - RegularNotification.fromList(it) - } - } - else -> super.readValueOfType(type, buffer) + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 128.toByte() -> { + return (readValue(buffer) as? List)?.let { + MessagingNotification.fromList(it) } - } - override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { - when (value) { - is MessagingNotification -> { - stream.write(128) - writeValue(stream, value.toList()) - } - is NotificationChannel -> { - stream.write(129) - writeValue(stream, value.toList()) - } - is NotificationEvent -> { - stream.write(130) - writeValue(stream, value.toList()) - } - is NotificationGroup -> { - stream.write(131) - writeValue(stream, value.toList()) - } - is NotificationI18nData -> { - stream.write(132) - writeValue(stream, value.toList()) - } - is NotificationMessage -> { - stream.write(133) - writeValue(stream, value.toList()) - } - is NotificationMessageContent -> { - stream.write(134) - writeValue(stream, value.toList()) - } - is RegularNotification -> { - stream.write(135) - writeValue(stream, value.toList()) - } - else -> super.writeValue(stream, value) + } + 129.toByte() -> { + return (readValue(buffer) as? List)?.let { + NotificationChannel.fromList(it) } + } + 130.toByte() -> { + return (readValue(buffer) as? List)?.let { + NotificationEvent.fromList(it) + } + } + 131.toByte() -> { + return (readValue(buffer) as? List)?.let { + NotificationGroup.fromList(it) + } + } + 132.toByte() -> { + return (readValue(buffer) as? List)?.let { + NotificationI18nData.fromList(it) + } + } + 133.toByte() -> { + return (readValue(buffer) as? List)?.let { + NotificationMessage.fromList(it) + } + } + 134.toByte() -> { + return (readValue(buffer) as? List)?.let { + NotificationMessageContent.fromList(it) + } + } + 135.toByte() -> { + return (readValue(buffer) as? List)?.let { + RegularNotification.fromList(it) + } + } + else -> super.readValueOfType(type, buffer) } + } + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + when (value) { + is MessagingNotification -> { + stream.write(128) + writeValue(stream, value.toList()) + } + is NotificationChannel -> { + stream.write(129) + writeValue(stream, value.toList()) + } + is NotificationEvent -> { + stream.write(130) + writeValue(stream, value.toList()) + } + is NotificationGroup -> { + stream.write(131) + writeValue(stream, value.toList()) + } + is NotificationI18nData -> { + stream.write(132) + writeValue(stream, value.toList()) + } + is NotificationMessage -> { + stream.write(133) + writeValue(stream, value.toList()) + } + is NotificationMessageContent -> { + stream.write(134) + writeValue(stream, value.toList()) + } + is RegularNotification -> { + stream.write(135) + writeValue(stream, value.toList()) + } + else -> super.writeValue(stream, value) + } + } } /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface MoxxyNotificationsApi { - /** Notification APIs */ - fun createNotificationGroups(groups: List) - fun deleteNotificationGroups(ids: List) - fun createNotificationChannels(channels: List) - fun deleteNotificationChannels(ids: List) - fun showMessagingNotification(notification: MessagingNotification) - fun showNotification(notification: RegularNotification) - fun dismissNotification(id: Long) - fun setNotificationSelfAvatar(path: String) - fun setNotificationI18n(data: NotificationI18nData) - fun notificationStub(event: NotificationEvent) + /** Notification APIs */ + fun createNotificationGroups(groups: List) + fun deleteNotificationGroups(ids: List) + fun createNotificationChannels(channels: List) + fun deleteNotificationChannels(ids: List) + fun showMessagingNotification(notification: MessagingNotification) + fun showNotification(notification: RegularNotification) + fun dismissNotification(id: Long) + fun setNotificationSelfAvatar(path: String) + fun setNotificationI18n(data: NotificationI18nData) + fun notificationStub(event: NotificationEvent) - companion object { - /** The codec used by MoxxyNotificationsApi. */ - val codec: MessageCodec by lazy { - MoxxyNotificationsApiCodec - } - - /** Sets up an instance of `MoxxyNotificationsApi` to handle messages through the `binaryMessenger`. */ - @Suppress("UNCHECKED_CAST") - fun setUp(binaryMessenger: BinaryMessenger, api: MoxxyNotificationsApi?) { - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyNotificationsApi.createNotificationGroups", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val groupsArg = args[0] as List - var wrapped: List - try { - api.createNotificationGroups(groupsArg) - 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.MoxxyNotificationsApi.deleteNotificationGroups", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val idsArg = args[0] as List - var wrapped: List - try { - api.deleteNotificationGroups(idsArg) - 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.MoxxyNotificationsApi.createNotificationChannels", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val channelsArg = args[0] as List - var wrapped: List - try { - api.createNotificationChannels(channelsArg) - 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.MoxxyNotificationsApi.deleteNotificationChannels", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val idsArg = args[0] as List - var wrapped: List - try { - api.deleteNotificationChannels(idsArg) - 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.MoxxyNotificationsApi.showMessagingNotification", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val notificationArg = args[0] as MessagingNotification - var wrapped: List - try { - api.showMessagingNotification(notificationArg) - 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.MoxxyNotificationsApi.showNotification", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val notificationArg = args[0] as RegularNotification - var wrapped: List - try { - api.showNotification(notificationArg) - 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.MoxxyNotificationsApi.dismissNotification", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val idArg = args[0].let { if (it is Int) it.toLong() else it as Long } - var wrapped: List - try { - api.dismissNotification(idArg) - 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.MoxxyNotificationsApi.setNotificationSelfAvatar", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val pathArg = args[0] as String - var wrapped: List - try { - api.setNotificationSelfAvatar(pathArg) - 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.MoxxyNotificationsApi.setNotificationI18n", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val dataArg = args[0] as NotificationI18nData - var wrapped: List - try { - api.setNotificationI18n(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.MoxxyNotificationsApi.notificationStub", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val eventArg = args[0] as NotificationEvent - var wrapped: List - try { - api.notificationStub(eventArg) - wrapped = listOf(null) - } catch (exception: Throwable) { - wrapped = wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - } + companion object { + /** The codec used by MoxxyNotificationsApi. */ + val codec: MessageCodec by lazy { + MoxxyNotificationsApiCodec } + /** Sets up an instance of `MoxxyNotificationsApi` to handle messages through the `binaryMessenger`. */ + @Suppress("UNCHECKED_CAST") + fun setUp(binaryMessenger: BinaryMessenger, api: MoxxyNotificationsApi?) { + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyNotificationsApi.createNotificationGroups", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val groupsArg = args[0] as List + var wrapped: List + try { + api.createNotificationGroups(groupsArg) + 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.MoxxyNotificationsApi.deleteNotificationGroups", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val idsArg = args[0] as List + var wrapped: List + try { + api.deleteNotificationGroups(idsArg) + 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.MoxxyNotificationsApi.createNotificationChannels", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val channelsArg = args[0] as List + var wrapped: List + try { + api.createNotificationChannels(channelsArg) + 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.MoxxyNotificationsApi.deleteNotificationChannels", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val idsArg = args[0] as List + var wrapped: List + try { + api.deleteNotificationChannels(idsArg) + 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.MoxxyNotificationsApi.showMessagingNotification", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val notificationArg = args[0] as MessagingNotification + var wrapped: List + try { + api.showMessagingNotification(notificationArg) + 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.MoxxyNotificationsApi.showNotification", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val notificationArg = args[0] as RegularNotification + var wrapped: List + try { + api.showNotification(notificationArg) + 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.MoxxyNotificationsApi.dismissNotification", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val idArg = args[0].let { if (it is Int) it.toLong() else it as Long } + var wrapped: List + try { + api.dismissNotification(idArg) + 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.MoxxyNotificationsApi.setNotificationSelfAvatar", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val pathArg = args[0] as String + var wrapped: List + try { + api.setNotificationSelfAvatar(pathArg) + 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.MoxxyNotificationsApi.setNotificationI18n", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val dataArg = args[0] as NotificationI18nData + var wrapped: List + try { + api.setNotificationI18n(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.MoxxyNotificationsApi.notificationStub", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val eventArg = args[0] as NotificationEvent + var wrapped: List + try { + api.notificationStub(eventArg) + wrapped = listOf(null) + } catch (exception: Throwable) { + wrapped = wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + } + } } diff --git a/lib/pigeon/background_service.g.dart b/lib/pigeon/background_service.g.dart index 16df43a..13c7cf6 100644 --- a/lib/pigeon/background_service.g.dart +++ b/lib/pigeon/background_service.g.dart @@ -20,10 +20,10 @@ class MoxxyBackgroundServiceApi { Future getExtraData() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.getExtraData', codec, + 'dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.getExtraData', + codec, binaryMessenger: _binaryMessenger); - final List? replyList = - await channel.send(null) as List?; + final List? replyList = await channel.send(null) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -47,7 +47,8 @@ class MoxxyBackgroundServiceApi { Future setNotificationBody(String arg_body) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.setNotificationBody', codec, + 'dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.setNotificationBody', + codec, binaryMessenger: _binaryMessenger); final List? replyList = await channel.send([arg_body]) as List?; @@ -69,7 +70,8 @@ class MoxxyBackgroundServiceApi { Future sendData(String arg_data) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.sendData', codec, + 'dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.sendData', + codec, binaryMessenger: _binaryMessenger); final List? replyList = await channel.send([arg_data]) as List?; @@ -93,8 +95,7 @@ class MoxxyBackgroundServiceApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.stop', codec, binaryMessenger: _binaryMessenger); - final List? replyList = - await channel.send(null) as List?; + final List? replyList = await channel.send(null) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', diff --git a/lib/src/service/background/isolate.dart b/lib/src/service/background/isolate.dart new file mode 100644 index 0000000..3c26bad --- /dev/null +++ b/lib/src/service/background/isolate.dart @@ -0,0 +1,53 @@ +import 'dart:convert'; +import 'dart:isolate'; +import 'dart:ui'; +import 'package:logging/logging.dart'; +import 'package:moxlib/moxlib.dart'; +import 'package:moxxy_native/src/service/background/base.dart'; +import 'package:moxxy_native/src/service/config.dart'; +import 'package:moxxy_native/src/service/datasender/types.dart'; +import 'package:uuid/uuid.dart'; + +class IsolateBackgroundService extends BackgroundService { + IsolateBackgroundService(this._sendPort); + final SendPort _sendPort; + final ReceivePort receivePort = ReceivePort(); + + /// A logger. + final Logger _log = Logger('IsolateBackgroundService'); + + @override + Future send(BackgroundEvent event, {String? id}) async { + final data = DataWrapper( + id ?? const Uuid().v4(), + event, + ); + + _sendPort.send(jsonEncode(data.toJson())); + } + + @override + Future init( + ServiceConfig config, + ) async { + // Ensure that the Dart executor is ready to use plugins + // NOTE: We're not allowed to use this here. Maybe reusing the RootIsolateToken + // (See IsolateForegroundService) helps? + // WidgetsFlutterBinding.ensureInitialized(); + DartPluginRegistrant.ensureInitialized(); + + // Register the channel for Foreground -> Service communication + receivePort.listen((data) async { + // TODO(Unknown): Maybe do something smarter like pigeon and use Lists instead of Maps + await config + .handleData(jsonDecode(data! as String) as Map); + }); + + // Start execution + _log.finest('Setup complete. Calling main entrypoint...'); + await config.entrypoint(config.initialLocale); + } + + @override + void setNotificationBody(String body) {} +} diff --git a/lib/src/service/datasender/isolate.dart b/lib/src/service/datasender/isolate.dart new file mode 100644 index 0000000..f5c192a --- /dev/null +++ b/lib/src/service/datasender/isolate.dart @@ -0,0 +1,15 @@ +import 'dart:convert'; +import 'dart:isolate'; +import 'package:moxlib/moxlib.dart'; +import 'package:moxxy_native/src/service/datasender/types.dart'; + +class IsolateForegroundServiceDataSender + extends AwaitableDataSender { + IsolateForegroundServiceDataSender(this._port); + final SendPort _port; + + @override + Future sendDataImpl(DataWrapper data) async { + _port.send(jsonEncode(data.toJson())); + } +} diff --git a/lib/src/service/datasender/types.dart b/lib/src/service/datasender/types.dart index 4a93cb6..ff7c6a3 100644 --- a/lib/src/service/datasender/types.dart +++ b/lib/src/service/datasender/types.dart @@ -1,8 +1,4 @@ -import 'dart:io'; import 'package:moxlib/moxlib.dart'; -import 'package:moxxy_native/pigeon/service.g.dart'; -import 'package:moxxy_native/src/service/datasender/pigeon.dart'; -import 'package:moxxy_native/src/service/exceptions.dart'; typedef ForegroundServiceDataSender = AwaitableDataSender; @@ -10,11 +6,3 @@ typedef ForegroundServiceDataSender abstract class BackgroundCommand implements JsonImplementation {} abstract class BackgroundEvent implements JsonImplementation {} - -ForegroundServiceDataSender getForegroundDataSender(MoxxyServiceApi api) { - if (Platform.isAndroid) { - return PigeonForegroundServiceDataSender(api); - } else { - throw UnsupportedPlatformException(); - } -} diff --git a/lib/src/service/entrypoints/base.dart b/lib/src/service/entrypoints/base.dart new file mode 100644 index 0000000..13616d4 --- /dev/null +++ b/lib/src/service/entrypoints/base.dart @@ -0,0 +1,28 @@ +import 'dart:io'; +import 'package:moxxy_native/src/service/config.dart'; +import 'package:moxxy_native/src/service/entrypoints/isolate.dart'; +import 'package:moxxy_native/src/service/entrypoints/pigeon.dart'; +import 'package:moxxy_native/src/service/exceptions.dart'; + +typedef PlatformEntrypointCallback = Future Function(dynamic); + +ServiceConfig getServiceConfig( + HandleEventCallback srvHandleData, + HandleEventCallback uiHandleData, + String initialLocale, +) { + PlatformEntrypointCallback entrypoint; + if (Platform.isAndroid) { + entrypoint = pigeonEntrypoint; + } else if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) { + entrypoint = isolateEntrypoint; + } else { + throw UnsupportedPlatformException(); + } + + return ServiceConfig( + entrypoint, + srvHandleData, + initialLocale, + ); +} diff --git a/lib/src/service/entrypoints/isolate.dart b/lib/src/service/entrypoints/isolate.dart new file mode 100644 index 0000000..9036b18 --- /dev/null +++ b/lib/src/service/entrypoints/isolate.dart @@ -0,0 +1,30 @@ +import 'dart:isolate'; +import 'package:flutter/services.dart'; +import 'package:get_it/get_it.dart'; +import 'package:moxxy_native/src/service/background/base.dart'; +import 'package:moxxy_native/src/service/background/isolate.dart'; +import 'package:moxxy_native/src/service/config.dart'; + +@pragma('vm:entry-point') +Future isolateEntrypoint(dynamic parameters) async { + parameters as List; + + final sendPort = parameters[0] as SendPort; + final config = ServiceConfig.fromString(parameters[1] as String); + + // This allows us to use the root isolate's method channels. + // See https://medium.com/flutter/introducing-background-isolate-channels-7a299609cad8 + BackgroundIsolateBinaryMessenger.ensureInitialized( + parameters[2] as RootIsolateToken, + ); + + // Set up the background service + final srv = IsolateBackgroundService(sendPort); + GetIt.I.registerSingleton(srv); + + // Reply back with the new send port + sendPort.send(srv.receivePort.sendPort); + + // Run the entrypoint + await srv.init(config); +} diff --git a/lib/src/service/entrypoints/pigeon.dart b/lib/src/service/entrypoints/pigeon.dart index da27b1b..0a1e00d 100644 --- a/lib/src/service/entrypoints/pigeon.dart +++ b/lib/src/service/entrypoints/pigeon.dart @@ -8,7 +8,7 @@ import 'package:moxxy_native/src/service/config.dart'; /// An entrypoint that should be used when the service runs /// in a new Flutter Engine. @pragma('vm:entry-point') -Future pigeonEntrypoint() async { +Future pigeonEntrypoint(dynamic _) async { // ignore: avoid_print print('androidEntrypoint: Called on new FlutterEngine'); diff --git a/lib/src/service/foreground/base.dart b/lib/src/service/foreground/base.dart index 0530800..dfcb1fa 100644 --- a/lib/src/service/foreground/base.dart +++ b/lib/src/service/foreground/base.dart @@ -3,6 +3,7 @@ import 'package:moxlib/moxlib.dart'; import 'package:moxxy_native/src/service/config.dart'; import 'package:moxxy_native/src/service/datasender/types.dart'; import 'package:moxxy_native/src/service/exceptions.dart'; +import 'package:moxxy_native/src/service/foreground/isolate.dart'; import 'package:moxxy_native/src/service/foreground/pigeon.dart'; /// Wrapper API that is only available to the UI isolate. @@ -40,6 +41,8 @@ ForegroundService getForegroundService() { if (_service == null) { if (Platform.isAndroid) { _service = PigeonForegroundService(); + } else if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) { + _service = IsolateForegroundService(); } else { throw UnsupportedPlatformException(); } diff --git a/lib/src/service/foreground/isolate.dart b/lib/src/service/foreground/isolate.dart new file mode 100644 index 0000000..5711578 --- /dev/null +++ b/lib/src/service/foreground/isolate.dart @@ -0,0 +1,98 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:isolate'; +import 'dart:ui'; +import 'package:logging/logging.dart'; +import 'package:moxxy_native/src/service/config.dart'; +import 'package:moxxy_native/src/service/datasender/isolate.dart'; +import 'package:moxxy_native/src/service/datasender/types.dart'; +import 'package:moxxy_native/src/service/entrypoints/isolate.dart'; +import 'package:moxxy_native/src/service/foreground/base.dart'; + +class IsolateForegroundService extends ForegroundService { + /// The port on which we receive data from the isolate. + final ReceivePort _receivePort = ReceivePort(); + + /// The port on which we send data to the isolate. + late final SendPort _sendPort; + + /// A completer that indicates when _sendPort has been set. + /// For more notes, see the comment in [start]. + Completer? _sendPortCompleter = Completer(); + + /// The data sender backing this class. + late final IsolateForegroundServiceDataSender _dataSender; + + /// A logger. + final Logger _log = Logger('IsolateForegroundService'); + + @override + Future attach( + HandleEventCallback handleData, + ) async { + _receivePort.asBroadcastStream().listen((data) async { + if (data is SendPort) { + // Set the send port. + _sendPort = data; + + // Resolve the waiting future. + assert( + _sendPortCompleter != null, + '_sendPort should only be received once!', + ); + _sendPortCompleter?.complete(); + return; + } + + await handleData( + jsonDecode(data! as String) as Map, + ); + }); + } + + @override + Future start( + ServiceConfig config, + HandleEventCallback uiHandleData, + ) async { + // Listen for events + await attach(uiHandleData); + + await Isolate.spawn( + isolateEntrypoint, + [ + _receivePort.sendPort, + config.toString(), + RootIsolateToken.instance!, + ], + ); + + // Wait for [_sendPort] to get set. + // The issue is that [_receivePort] provides a stream that only one listener can listen to. + // This means that we cannot do `await _receivePort.first`. To work around this, we just cram + // an approximation of `_receivePort.first` into the actual listener. + await _sendPortCompleter!.future; + _sendPortCompleter = null; + + // Create the data sender + _dataSender = IsolateForegroundServiceDataSender(_sendPort); + _log.finest('Background service started...'); + } + + @override + Future isRunning() async => false; + + @override + ForegroundServiceDataSender getDataSender() => _dataSender; + + @override + Future send( + BackgroundCommand command, { + bool awaitable = true, + }) { + return _dataSender.sendData( + command, + awaitable: awaitable, + ); + } +}