From fb9dab3d1e79d09becd2e204a9dd9a4e5adc330b Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Fri, 28 Jul 2023 13:54:57 +0200 Subject: [PATCH] Implement streaming data into Flutter --- example/lib/main.dart | 4 + .../me/polynom/moxplatform_android/Api.java | 150 +++++++++++++++++- .../polynom/moxplatform_android/Constants.kt | 1 + .../MoxplatformAndroidPlugin.java | 33 +++- .../NotificationReceiver.kt | 59 +++++-- .../moxplatform_android/Notifications.kt | 27 ++-- .../lib/src/notifications_android.dart | 21 ++- .../lib/src/api.g.dart | 75 ++++++++- .../lib/src/notifications.dart | 3 + .../lib/src/notifications_stub.dart | 6 + pigeons/api.dart | 27 ++++ 11 files changed, 374 insertions(+), 32 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 4602aac..ada36f7 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -42,6 +42,10 @@ class MyAppState extends State { await Permission.notification.request(); await MoxplatformPlugin.notifications.createNotificationChannel("Test notification channel", channelId, false); + + MoxplatformPlugin.notifications.getEventStream().listen((event) { + print('NotificationEvent(type: ${event.type}, jid: ${event.jid}, payload: ${event.payload})'); + }); } @override diff --git a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Api.java b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Api.java index c1b3eb7..f500f94 100644 --- a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Api.java +++ b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Api.java @@ -57,6 +57,18 @@ public class Api { return errorList; } + public enum NotificationEventType { + MARK_AS_READ(0), + REPLY(1), + OPEN(2); + + final int index; + + private NotificationEventType(final int index) { + this.index = index; + } + } + /** Generated class from Pigeon that represents data sent in messages. */ public static final class NotificationMessageContent { /** The textual body of the message. */ @@ -441,6 +453,107 @@ public class Api { } } + /** Generated class from Pigeon that represents data sent in messages. */ + public static final class NotificationEvent { + /** The JID the notification was for. */ + private @NonNull String jid; + + public @NonNull String getJid() { + return jid; + } + + public void setJid(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"jid\" is null."); + } + this.jid = setterArg; + } + + /** The type of event. */ + private @NonNull NotificationEventType type; + + public @NonNull NotificationEventType getType() { + return type; + } + + public void setType(@NonNull NotificationEventType setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"type\" is null."); + } + this.type = setterArg; + } + + /** + * An optional payload. + * - type == NotificationType.reply: The reply message text. + * Otherwise: undefined. + */ + private @Nullable String payload; + + public @Nullable String getPayload() { + return payload; + } + + public void setPayload(@Nullable String setterArg) { + this.payload = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + NotificationEvent() {} + + public static final class Builder { + + private @Nullable String jid; + + public @NonNull Builder setJid(@NonNull String setterArg) { + this.jid = setterArg; + return this; + } + + private @Nullable NotificationEventType type; + + public @NonNull Builder setType(@NonNull NotificationEventType setterArg) { + this.type = setterArg; + return this; + } + + private @Nullable String payload; + + public @NonNull Builder setPayload(@Nullable String setterArg) { + this.payload = setterArg; + return this; + } + + public @NonNull NotificationEvent build() { + NotificationEvent pigeonReturn = new NotificationEvent(); + pigeonReturn.setJid(jid); + pigeonReturn.setType(type); + pigeonReturn.setPayload(payload); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(3); + toListResult.add(jid); + toListResult.add(type == null ? null : type.index); + toListResult.add(payload); + return toListResult; + } + + static @NonNull NotificationEvent fromList(@NonNull ArrayList list) { + NotificationEvent pigeonResult = new NotificationEvent(); + Object jid = list.get(0); + pigeonResult.setJid((String) jid); + Object type = list.get(1); + pigeonResult.setType(type == null ? null : NotificationEventType.values()[(int) type]); + Object payload = list.get(2); + pigeonResult.setPayload((String) payload); + return pigeonResult; + } + } + private static class MoxplatformApiCodec extends StandardMessageCodec { public static final MoxplatformApiCodec INSTANCE = new MoxplatformApiCodec(); @@ -452,8 +565,10 @@ public class Api { case (byte) 128: return MessagingNotification.fromList((ArrayList) readValue(buffer)); case (byte) 129: - return NotificationMessage.fromList((ArrayList) readValue(buffer)); + return NotificationEvent.fromList((ArrayList) readValue(buffer)); case (byte) 130: + return NotificationMessage.fromList((ArrayList) readValue(buffer)); + case (byte) 131: return NotificationMessageContent.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); @@ -465,11 +580,14 @@ public class Api { if (value instanceof MessagingNotification) { stream.write(128); writeValue(stream, ((MessagingNotification) value).toList()); - } else if (value instanceof NotificationMessage) { + } else if (value instanceof NotificationEvent) { stream.write(129); + writeValue(stream, ((NotificationEvent) value).toList()); + } else if (value instanceof NotificationMessage) { + stream.write(130); writeValue(stream, ((NotificationMessage) value).toList()); } else if (value instanceof NotificationMessageContent) { - stream.write(130); + stream.write(131); writeValue(stream, ((NotificationMessageContent) value).toList()); } else { super.writeValue(stream, value); @@ -490,6 +608,8 @@ public class Api { @NonNull String getCacheDataPath(); + void eventStub(@NonNull NotificationEvent event); + /** The codec used by MoxplatformApi. */ static @NonNull MessageCodec getCodec() { return MoxplatformApiCodec.INSTANCE; @@ -580,6 +700,30 @@ public class Api { String output = api.getCacheDataPath(); wrapped.add(0, output); } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.eventStub", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + NotificationEvent eventArg = (NotificationEvent) args.get(0); + try { + api.eventStub(eventArg); + wrapped.add(0, null); + } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; diff --git a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Constants.kt b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Constants.kt index 964dd48..4c70206 100644 --- a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Constants.kt +++ b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Constants.kt @@ -6,6 +6,7 @@ const val TAG = "Moxplatform" // The size of the buffer to hashing, encryption, and decryption in bytes. const val BUFFER_SIZE = 8096 +const val REPLY_ACTION = "reply"; // The data key for text entered in the notification's reply field const val REPLY_TEXT_KEY = "key_reply_text" diff --git a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/MoxplatformAndroidPlugin.java b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/MoxplatformAndroidPlugin.java index 0515d6b..e9d06ea 100644 --- a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/MoxplatformAndroidPlugin.java +++ b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/MoxplatformAndroidPlugin.java @@ -25,6 +25,9 @@ import java.util.List; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.service.ServiceAware; import io.flutter.embedding.engine.plugins.service.ServicePluginBinding; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.EventChannel.EventSink; +import io.flutter.plugin.common.EventChannel.StreamHandler; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; @@ -34,7 +37,7 @@ import io.flutter.plugin.common.JSONMethodCodec; import kotlin.Unit; import kotlin.jvm.functions.Function1; -public class MoxplatformAndroidPlugin extends BroadcastReceiver implements FlutterPlugin, MethodCallHandler, ServiceAware, MoxplatformApi { + public class MoxplatformAndroidPlugin extends BroadcastReceiver implements FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler, ServiceAware, MoxplatformApi { public static final String entrypointKey = "entrypoint_handle"; public static final String extraDataKey = "extra_data"; private static final String autoStartAtBootKey = "auto_start_at_boot"; @@ -46,7 +49,10 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt private static final List _instances = new ArrayList<>(); private BackgroundService service; private MethodChannel channel; - private Context context; + private static EventChannel notificationChannel; + public static EventSink notificationSink; + + private Context context; public MoxplatformAndroidPlugin() { _instances.add(this); @@ -58,6 +64,12 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt channel.setMethodCallHandler(this); context = flutterPluginBinding.getApplicationContext(); + notificationChannel = new EventChannel(flutterPluginBinding.getBinaryMessenger(), "me.polynom/notification_stream"); + notificationChannel.setStreamHandler( + this + ); + + LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(this.context); localBroadcastManager.registerReceiver(this, new IntentFilter(methodChannelKey)); @@ -78,6 +90,18 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt Log.d(TAG, "Registered against registrar"); } + @Override + public void onCancel(Object arguments) { + Log.d(TAG, "Removed listener"); + notificationSink = null; + } + + @Override + public void onListen(Object arguments, EventChannel.EventSink eventSink) { + Log.d(TAG, "Attached listener"); + notificationSink = eventSink; + } + /// Store the entrypoint handle and extra data for the background service. private void configure(long entrypointHandle, String extraData) { SharedPreferences prefs = context.getSharedPreferences(sharedPrefKey, Context.MODE_PRIVATE); @@ -292,4 +316,9 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt public String getCacheDataPath() { return context.getCacheDir().getPath(); } + + @Override + public void eventStub(@NonNull NotificationEvent event) { + // Stub to trick pigeon into + } } diff --git a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/NotificationReceiver.kt b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/NotificationReceiver.kt index cd022ad..a925263 100644 --- a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/NotificationReceiver.kt +++ b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/NotificationReceiver.kt @@ -7,22 +7,59 @@ import android.content.Intent import android.util.Log import androidx.core.app.NotificationManagerCompat import androidx.core.app.RemoteInput +import me.polynom.moxplatform_android.Api.NotificationEvent class NotificationReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - // If it is a mark as read, dismiss the entire notification and - // send a notification to the app. - // TODO: Notify app - if (intent.action == MARK_AS_READ_ACTION) { - Log.d("NotificationReceiver", "Marking ${intent.getStringExtra("jid")} as read") - NotificationManagerCompat.from(context).cancel(intent.getLongExtra(MARK_AS_READ_ID_KEY, -1).toInt()) - return + private fun handleMarkAsRead(context: Context, intent: Intent) { + Log.d("NotificationReceiver", "Marking ${intent.getStringExtra("jid")} as read") + val jidWrapper = intent.getStringExtra("jid") ?: "" + NotificationManagerCompat.from(context).cancel(intent.getLongExtra(MARK_AS_READ_ID_KEY, -1).toInt()) + MoxplatformAndroidPlugin.notificationSink?.success( + NotificationEvent().apply { + // TODO: Use constant for key + // TODO: Fix + jid = jidWrapper + type = Api.NotificationEventType.MARK_AS_READ + payload = null + }.toList() + ) + + // Dismiss the notification + val notificationId = intent.getLongExtra("notification_id", -1).toInt() + if (notificationId != -1) { + NotificationManagerCompat.from(context).cancel( + notificationId, + ) + } else { + Log.e("NotificationReceiver", "No id specified. Cannot dismiss notification") } + } + private fun handleReply(context: Context, intent: Intent) { + val jidWrapper = intent.getStringExtra("jid") ?: "" val remoteInput = RemoteInput.getResultsFromIntent(intent) ?: return - - val title = remoteInput.getCharSequence(REPLY_TEXT_KEY).toString() - Log.d("NotificationReceiver", title) + Log.d("NotificationReceiver", "Got a reply for ${jidWrapper}") // TODO: Notify app + MoxplatformAndroidPlugin.notificationSink?.success( + NotificationEvent().apply { + // TODO: Use constant for key + jid = jidWrapper + type = Api.NotificationEventType.REPLY + payload = remoteInput.getCharSequence(REPLY_TEXT_KEY).toString() + }.toList() + ) + + // TODO: Update the notification to prevent showing the spinner + } + + override fun onReceive(context: Context, intent: Intent) { + // TODO: We need to be careful to ensure that the Flutter engine is running. + // If it's not, we have to start it. However, that's only an issue when we expect to + // receive notifications while not running, i.e. Push Notifications. + when (intent.action) { + MARK_AS_READ_ACTION -> handleMarkAsRead(context, intent) + REPLY_ACTION -> handleReply(context, intent) + // TODO: Handle tap + } } } \ No newline at end of file diff --git a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Notifications.kt b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Notifications.kt index 140b629..034d53e 100644 --- a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Notifications.kt +++ b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Notifications.kt @@ -20,7 +20,11 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif // TODO: i18n setLabel("Reply") }.build() - val replyIntent = Intent(context, NotificationReceiver::class.java) + val replyIntent = Intent(context, NotificationReceiver::class.java).apply { + action = REPLY_ACTION + // TODO: Use a constant + putExtra("jid", notification.jid) + } val replyPendingIntent = PendingIntent.getBroadcast( context.applicationContext, 0, @@ -40,20 +44,29 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif // -> Mark as read action val markAsReadIntent = Intent(context, NotificationReceiver::class.java).apply { action = MARK_AS_READ_ACTION - // TODO: Put the JID here + // TODO: Use a constant putExtra("jid", notification.jid) + putExtra("notification_id", notification.id) } val markAsReadPendingIntent = PendingIntent.getBroadcast( context.applicationContext, 0, markAsReadIntent, - 0, + PendingIntent.FLAG_UPDATE_CURRENT, ) + val markAsReadAction = NotificationCompat.Action.Builder( + // TODO: Wrong icon + R.drawable.ic_service_icon, + // TODO: i18n + "Mark as read", + markAsReadPendingIntent, + ).build() // -> Tap action // Thanks https://github.com/MaikuB/flutter_local_notifications/blob/master/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java#L246 // TODO: Copy the interface of awesome_notifications val tapIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)!!.apply { + // TODO: Use a constant putExtra("jid", notification.jid) } val tapPendingIntent = PendingIntent.getActivity( @@ -115,13 +128,7 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif // Notification actions addAction(replyAction) - addAction( - // TODO: Wrong icon - R.drawable.ic_service_icon, - // TODO: i18n - "Mark as read", - markAsReadPendingIntent, - ) + addAction(markAsReadAction) }.build() // Post the notification diff --git a/packages/moxplatform_android/lib/src/notifications_android.dart b/packages/moxplatform_android/lib/src/notifications_android.dart index 37246b2..2a2243e 100644 --- a/packages/moxplatform_android/lib/src/notifications_android.dart +++ b/packages/moxplatform_android/lib/src/notifications_android.dart @@ -1,17 +1,32 @@ +import 'dart:async'; +import 'package:flutter/services.dart'; import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart'; class AndroidNotificationsImplementation extends NotificationsImplementation { final MoxplatformApi _api = MoxplatformApi(); + final EventChannel _channel = + const EventChannel('me.polynom/notification_stream'); @override - Future createNotificationChannel(String title, String id, bool urgent) async { + Future createNotificationChannel( + String title, + String id, + bool urgent, + ) async { return _api.createNotificationChannel(title, id, urgent); } - @override - Future showMessagingNotification(MessagingNotification notification) async { + Future showMessagingNotification( + MessagingNotification notification, + ) async { return _api.showMessagingNotification(notification); } + + @override + Stream getEventStream() => _channel + .receiveBroadcastStream() + .cast() + .map(NotificationEvent.decode); } diff --git a/packages/moxplatform_platform_interface/lib/src/api.g.dart b/packages/moxplatform_platform_interface/lib/src/api.g.dart index 089454a..96ea483 100644 --- a/packages/moxplatform_platform_interface/lib/src/api.g.dart +++ b/packages/moxplatform_platform_interface/lib/src/api.g.dart @@ -8,6 +8,12 @@ import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; +enum NotificationEventType { + markAsRead, + reply, + open, +} + class NotificationMessageContent { NotificationMessageContent({ this.body, @@ -133,6 +139,42 @@ class MessagingNotification { } } +class NotificationEvent { + NotificationEvent({ + required this.jid, + required this.type, + this.payload, + }); + + /// The JID the notification was for. + String jid; + + /// The type of event. + NotificationEventType type; + + /// An optional payload. + /// - type == NotificationType.reply: The reply message text. + /// Otherwise: undefined. + String? payload; + + Object encode() { + return [ + jid, + type.index, + payload, + ]; + } + + static NotificationEvent decode(Object result) { + result as List; + return NotificationEvent( + jid: result[0]! as String, + type: NotificationEventType.values[result[1]! as int], + payload: result[2] as String?, + ); + } +} + class _MoxplatformApiCodec extends StandardMessageCodec { const _MoxplatformApiCodec(); @override @@ -140,12 +182,15 @@ class _MoxplatformApiCodec extends StandardMessageCodec { if (value is MessagingNotification) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else if (value is NotificationMessage) { + } else if (value is NotificationEvent) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is NotificationMessageContent) { + } else if (value is NotificationMessage) { buffer.putUint8(130); writeValue(buffer, value.encode()); + } else if (value is NotificationMessageContent) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -157,8 +202,10 @@ class _MoxplatformApiCodec extends StandardMessageCodec { case 128: return MessagingNotification.decode(readValue(buffer)!); case 129: - return NotificationMessage.decode(readValue(buffer)!); + return NotificationEvent.decode(readValue(buffer)!); case 130: + return NotificationMessage.decode(readValue(buffer)!); + case 131: return NotificationMessageContent.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -273,4 +320,26 @@ class MoxplatformApi { return (replyList[0] as String?)!; } } + + Future eventStub(NotificationEvent arg_event) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.eventStub', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_event]) 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/packages/moxplatform_platform_interface/lib/src/notifications.dart b/packages/moxplatform_platform_interface/lib/src/notifications.dart index d12cfb8..8b5ff48 100644 --- a/packages/moxplatform_platform_interface/lib/src/notifications.dart +++ b/packages/moxplatform_platform_interface/lib/src/notifications.dart @@ -1,7 +1,10 @@ +import 'dart:async'; import 'package:moxplatform_platform_interface/src/api.g.dart'; abstract class NotificationsImplementation { Future createNotificationChannel(String title, String id, bool urgent); Future showMessagingNotification(MessagingNotification notification); + + Stream getEventStream(); } diff --git a/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart b/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart index b912f10..c6dd341 100644 --- a/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart +++ b/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:moxplatform_platform_interface/src/api.g.dart'; import 'package:moxplatform_platform_interface/src/notifications.dart'; @@ -7,4 +8,9 @@ class StubNotificationsImplementation extends NotificationsImplementation { @override Future showMessagingNotification(MessagingNotification notification) async {} + + @override + Stream getEventStream() { + return StreamController().stream; + } } diff --git a/pigeons/api.dart b/pigeons/api.dart index 14d8a61..7634510 100644 --- a/pigeons/api.dart +++ b/pigeons/api.dart @@ -72,6 +72,31 @@ class MessagingNotification { final List messages; } +enum NotificationEventType { + markAsRead, + reply, + open, +} + +class NotificationEvent { + const NotificationEvent( + this.jid, + this.type, + this.payload, + ); + + /// The JID the notification was for. + final String jid; + + /// The type of event. + final NotificationEventType type; + + /// An optional payload. + /// - type == NotificationType.reply: The reply message text. + /// Otherwise: undefined. + final String? payload; +} + @HostApi() abstract class MoxplatformApi { void createNotificationChannel(String title, String id, bool urgent); @@ -81,4 +106,6 @@ abstract class MoxplatformApi { String getPersistentDataPath(); String getCacheDataPath(); + + void eventStub(NotificationEvent event); }