Implement streaming data into Flutter

This commit is contained in:
2023-07-28 13:54:57 +02:00
parent da851a985b
commit fb9dab3d1e
11 changed files with 374 additions and 32 deletions

View File

@@ -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<Object> toList() {
ArrayList<Object> toListResult = new ArrayList<Object>(3);
toListResult.add(jid);
toListResult.add(type == null ? null : type.index);
toListResult.add(payload);
return toListResult;
}
static @NonNull NotificationEvent fromList(@NonNull ArrayList<Object> 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<Object>) readValue(buffer));
case (byte) 129:
return NotificationMessage.fromList((ArrayList<Object>) readValue(buffer));
return NotificationEvent.fromList((ArrayList<Object>) readValue(buffer));
case (byte) 130:
return NotificationMessage.fromList((ArrayList<Object>) readValue(buffer));
case (byte) 131:
return NotificationMessageContent.fromList((ArrayList<Object>) 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<Object> getCodec() {
return MoxplatformApiCodec.INSTANCE;
@@ -580,6 +700,30 @@ public class Api {
String output = api.getCacheDataPath();
wrapped.add(0, output);
}
catch (Throwable exception) {
ArrayList<Object> wrappedError = wrapError(exception);
wrapped = wrappedError;
}
reply.reply(wrapped);
});
} else {
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger, "dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.eventStub", getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
ArrayList<Object> args = (ArrayList<Object>) message;
NotificationEvent eventArg = (NotificationEvent) args.get(0);
try {
api.eventStub(eventArg);
wrapped.add(0, null);
}
catch (Throwable exception) {
ArrayList<Object> wrappedError = wrapError(exception);
wrapped = wrappedError;

View File

@@ -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"

View File

@@ -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<MoxplatformAndroidPlugin> _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
}
}

View File

@@ -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
}
}
}

View File

@@ -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

View File

@@ -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<void> createNotificationChannel(String title, String id, bool urgent) async {
Future<void> createNotificationChannel(
String title,
String id,
bool urgent,
) async {
return _api.createNotificationChannel(title, id, urgent);
}
@override
Future<void> showMessagingNotification(MessagingNotification notification) async {
Future<void> showMessagingNotification(
MessagingNotification notification,
) async {
return _api.showMessagingNotification(notification);
}
@override
Stream<NotificationEvent> getEventStream() => _channel
.receiveBroadcastStream()
.cast<Object>()
.map(NotificationEvent.decode);
}