From 1771c0e1b6ea58df236b7f78139ea1006e738c62 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Thu, 27 Jul 2023 20:45:09 +0200 Subject: [PATCH 01/23] Basic stuff --- example/android/app/build.gradle | 2 +- .../android/app/src/main/AndroidManifest.xml | 2 +- example/lib/main.dart | 83 ++- example/pubspec.yaml | 2 + packages/moxplatform/lib/src/plugin.dart | 1 + .../moxplatform_android/android/build.gradle | 3 +- .../android/src/main/AndroidManifest.xml | 12 + .../polynom/moxplatform_android/Constants.kt | 8 + .../moxplatform_android/FileProvider.kt | 6 + .../MoxplatformAndroidPlugin.java | 108 +++- .../NotificationReceiver.kt | 28 + .../moxplatform_android/Notifications.java | 520 ++++++++++++++++++ .../android/src/main/res/xml/file_paths.xml | 4 + .../lib/src/notifications_android.dart | 17 + .../lib/src/plugin_android.dart | 2 + packages/moxplatform_android/pubspec.yaml | 1 + .../lib/moxplatform_platform_interface.dart | 3 + .../lib/src/interface.dart | 3 + .../lib/src/notifications.dart | 7 + .../lib/src/notifications.g.dart | 216 ++++++++ .../lib/src/notifications_stub.dart | 10 + .../lib/src/contacts_android.dart | 0 .../lib/src/contacts.dart | 0 .../lib/src/contacts_stub.dart | 0 pigeons/notifications.dart | 77 +++ pubspec.yaml | 1 + 26 files changed, 1100 insertions(+), 16 deletions(-) create mode 100644 packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/FileProvider.kt create mode 100644 packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/NotificationReceiver.kt create mode 100644 packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Notifications.java create mode 100644 packages/moxplatform_android/android/src/main/res/xml/file_paths.xml create mode 100644 packages/moxplatform_android/lib/src/notifications_android.dart create mode 100644 packages/moxplatform_platform_interface/lib/src/notifications.dart create mode 100644 packages/moxplatform_platform_interface/lib/src/notifications.g.dart create mode 100644 packages/moxplatform_platform_interface/lib/src/notifications_stub.dart delete mode 100644 packages/moxplatform_platform_interface/packages/moxplatform_android/lib/src/contacts_android.dart delete mode 100644 packages/moxplatform_platform_interface/packages/moxplatform_platform_interface/lib/src/contacts.dart delete mode 100644 packages/moxplatform_platform_interface/packages/moxplatform_platform_interface/lib/src/contacts_stub.dart create mode 100644 pigeons/notifications.dart diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index ef7cb5a..41b0a6a 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -47,7 +47,7 @@ android { applicationId "com.example.example" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. - minSdkVersion flutter.minSdkVersion + minSdkVersion 26 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 0ef6ec2..62ae7c1 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -34,5 +34,5 @@ - + diff --git a/example/lib/main.dart b/example/lib/main.dart index e598bd7..eeeb251 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,17 +1,49 @@ import 'dart:io'; +import 'dart:math'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:file_picker/file_picker.dart'; import 'package:moxplatform/moxplatform.dart'; import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:file_picker/file_picker.dart'; + +/// The id of the notification channel. +const channelId = "me.polynom.moxplatform.testing3"; void main() { runApp(const MyApp()); } -class MyApp extends StatelessWidget { +class Sender { + const Sender(this.name, this.jid); + + final String name; + + final String jid; +} + +class MyApp extends StatefulWidget { const MyApp({Key? key}) : super(key: key); + @override + MyAppState createState() => MyAppState(); +} + +class MyAppState extends State { + @override + void initState() { + super.initState(); + + initStateAsync(); + } + + Future initStateAsync() async { + await Permission.notification.request(); + + await MoxplatformPlugin.notifications.createNotificationChannel("Test notification channel", channelId, false); + } + @override Widget build(BuildContext context) { return MaterialApp( @@ -19,13 +51,23 @@ class MyApp extends StatelessWidget { theme: ThemeData( primarySwatch: Colors.blue, ), - home: const MyHomePage(), + home: MyHomePage(), ); } } class MyHomePage extends StatelessWidget { - const MyHomePage({super.key}); + MyHomePage({super.key}); + + /// List of "Message senders". + final List senders = const [ + Sender('Mash Kyrielight', 'mash@example.org'), + Sender('Rio Tsukatsuki', 'rio@millenium'), + Sender('Raiden Shogun', 'raiden@tevhat'), + ]; + + /// List of sent messages. + List messages = List.empty(growable: true); Future _cryptoTest() async { final result = await FilePicker.platform.pickFiles(); @@ -98,6 +140,41 @@ class MyHomePage extends StatelessWidget { }, child: const Text('Test recordSentMessage (notes fallback)'), ), + ElevatedButton( + onPressed: () async { + final result = await FilePicker.platform.pickFiles( + type: FileType.image, + ); + print('Picked file: ${result?.files.single.path}'); + + // Create a new message. + final senderIndex = Random().nextInt(senders.length); + final time = DateTime.now().millisecondsSinceEpoch; + messages.add( + NotificationMessage( + jid: senders[senderIndex].jid, + sender: senders[senderIndex].name, + content: NotificationMessageContent( + body: result != null ? null : 'Message #${messages.length}', + mime: 'image/jpeg', + path: result?.files.single.path, + ), + timestamp: time, + ) + ); + + await Future.delayed(const Duration(seconds: 4)); + await MoxplatformPlugin.notifications.showMessagingNotification( + MessagingNotification( + id: 2343, + title: 'Test conversation', + messages: messages, + channelId: channelId, + ), + ); + }, + child: const Text('Show messaging notification'), + ), ], ), ), diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 692931c..955dd3a 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -38,6 +38,8 @@ dependencies: version: 0.1.17+1 file_picker: 5.2.0+1 + + permission_handler: 10.4.3 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. diff --git a/packages/moxplatform/lib/src/plugin.dart b/packages/moxplatform/lib/src/plugin.dart index d081d70..bc3d6c1 100644 --- a/packages/moxplatform/lib/src/plugin.dart +++ b/packages/moxplatform/lib/src/plugin.dart @@ -5,4 +5,5 @@ class MoxplatformPlugin { static MediaScannerImplementation get media => MoxplatformInterface.media; static CryptographyImplementation get crypto => MoxplatformInterface.crypto; static ContactsImplementation get contacts => MoxplatformInterface.contacts; + static NotificationsImplementation get notifications => MoxplatformInterface.notifications; } diff --git a/packages/moxplatform_android/android/build.gradle b/packages/moxplatform_android/android/build.gradle index 89b86f2..0f01fe5 100644 --- a/packages/moxplatform_android/android/build.gradle +++ b/packages/moxplatform_android/android/build.gradle @@ -35,7 +35,8 @@ android { } defaultConfig { - minSdkVersion 16 + // What Moxxy currently uses + minSdkVersion 26 } } diff --git a/packages/moxplatform_android/android/src/main/AndroidManifest.xml b/packages/moxplatform_android/android/src/main/AndroidManifest.xml index 2c55e6b..4626f0e 100644 --- a/packages/moxplatform_android/android/src/main/AndroidManifest.xml +++ b/packages/moxplatform_android/android/src/main/AndroidManifest.xml @@ -1,11 +1,22 @@ + + + + + + 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 21ac98b..964dd48 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,14 @@ const val TAG = "Moxplatform" // The size of the buffer to hashing, encryption, and decryption in bytes. const val BUFFER_SIZE = 8096 +// The data key for text entered in the notification's reply field +const val REPLY_TEXT_KEY = "key_reply_text" + +// The action for pressing the "Mark as read" button on a notification +const val MARK_AS_READ_ACTION = "mark_as_read" +// The key for the notification id to mark as read +const val MARK_AS_READ_ID_KEY = "notification_id" + // TODO: Maybe try again to rewrite the entire plugin in Kotlin //const val METHOD_CHANNEL_KEY = "me.polynom.moxplatform_android" //const val BACKGROUND_METHOD_CHANNEL_KEY = METHOD_CHANNEL_KEY + "_bg" diff --git a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/FileProvider.kt b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/FileProvider.kt new file mode 100644 index 0000000..f3cb371 --- /dev/null +++ b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/FileProvider.kt @@ -0,0 +1,6 @@ +package me.polynom.moxplatform_android + +import androidx.core.content.FileProvider + +class MoxplatformFileProvider : FileProvider(R.xml.file_paths) { +} \ No newline at end of file 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 0aa1b77..c8fb6d8 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 @@ -1,31 +1,39 @@ package me.polynom.moxplatform_android; +import static androidx.core.content.ContextCompat.getSystemService; +import static me.polynom.moxplatform_android.ConstantsKt.MARK_AS_READ_ACTION; +import static me.polynom.moxplatform_android.ConstantsKt.MARK_AS_READ_ID_KEY; +import static me.polynom.moxplatform_android.ConstantsKt.REPLY_TEXT_KEY; import static me.polynom.moxplatform_android.RecordSentMessageKt.recordSentMessage; import static me.polynom.moxplatform_android.CryptoKt.*; +import me.polynom.moxplatform_android.Notifications.*; import android.app.ActivityManager; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.graphics.BitmapFactory; +import android.net.Uri; import android.util.Log; import androidx.annotation.NonNull; +import androidx.core.app.RemoteInput; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; +import androidx.core.app.Person; import androidx.core.content.ContextCompat; +import androidx.core.graphics.drawable.IconCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import java.io.FileInputStream; -import java.security.MessageDigest; +import java.io.File; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import javax.crypto.Cipher; -import javax.crypto.CipherOutputStream; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; - import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.service.ServiceAware; import io.flutter.embedding.engine.plugins.service.ServicePluginBinding; @@ -35,8 +43,10 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugin.common.PluginRegistry.Registrar; import io.flutter.plugin.common.JSONMethodCodec; +import kotlin.Unit; +import kotlin.jvm.functions.Function1; -public class MoxplatformAndroidPlugin extends BroadcastReceiver implements FlutterPlugin, MethodCallHandler, ServiceAware { +public class MoxplatformAndroidPlugin extends BroadcastReceiver implements FlutterPlugin, MethodCallHandler, ServiceAware, NotificationsImplementationApi { public static final String entrypointKey = "entrypoint_handle"; public static final String extraDataKey = "extra_data"; private static final String autoStartAtBootKey = "auto_start_at_boot"; @@ -50,6 +60,8 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt private MethodChannel channel; private Context context; + private FileProvider provider = new FileProvider(); + public MoxplatformAndroidPlugin() { _instances.add(this); } @@ -63,6 +75,8 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(this.context); localBroadcastManager.registerReceiver(this, new IntentFilter(methodChannelKey)); + NotificationsImplementationApi.setup(flutterPluginBinding.getBinaryMessenger(), this); + Log.d(TAG, "Attached to engine"); } @@ -117,7 +131,7 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt } @Override - public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { + public void onMethodCall(@NonNull MethodCall call, @NonNull io.flutter.plugin.common.MethodChannel.Result result) { switch (call.method) { case "configure": ArrayList args = (ArrayList) call.arguments; @@ -262,4 +276,78 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt Log.d(TAG, "Detached from service"); this.service = null; } + + @Override + public void createNotificationChannel(@NonNull String title, @NonNull String id, @NonNull Boolean urgent) { + final NotificationChannel channel = new NotificationChannel( + id, + title, + urgent ? NotificationManager.IMPORTANCE_HIGH : NotificationManager.IMPORTANCE_DEFAULT + ); + channel.enableVibration(true); + channel.enableLights(true); + final NotificationManager manager = getSystemService(context, NotificationManager.class); + manager.createNotificationChannel(channel); + } + + @Override + public void showMessagingNotification(@NonNull MessagingNotification notification) { + // Create a reply button + // TODO: i18n + RemoteInput remoteInput = new RemoteInput.Builder(REPLY_TEXT_KEY).setLabel("Reply").build(); + final Intent replyIntent = new Intent(context, NotificationReceiver.class); + final PendingIntent replyPendingIntent = PendingIntent.getBroadcast(context.getApplicationContext(), 0, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT); + // TODO: i18n + // TODO: Correct icon + final NotificationCompat.Action action = new NotificationCompat.Action.Builder(R.drawable.ic_service_icon, "Reply", replyPendingIntent) + .addRemoteInput(remoteInput) + .build(); + + // Create the "mark as read" button + final Intent markAsReadIntent = new Intent(context, NotificationReceiver.class); + markAsReadIntent.setAction(MARK_AS_READ_ACTION); + markAsReadIntent.putExtra(MARK_AS_READ_ID_KEY, notification.getId()); + // TODO: Replace with something more useful + markAsReadIntent.putExtra("title", notification.getTitle()); + final PendingIntent markAsReadPendingIntent = PendingIntent.getBroadcast(context.getApplicationContext(), 0, readIntent,PendingIntent.FLAG_CANCEL_CURRENT); + + final NotificationCompat.MessagingStyle style = new NotificationCompat.MessagingStyle("Me") + .setConversationTitle(notification.getTitle()); + for (final NotificationMessage message : notification.getMessages()) { + // Build the sender of the message + final Person.Builder personBuilder = new Person.Builder() + .setName(message.getSender()) + .setKey(message.getJid()); + if (message.getAvatarPath() != null) { + final IconCompat icon = IconCompat.createWithAdaptiveBitmap( + BitmapFactory.decodeFile(message.getAvatarPath()) + ); + personBuilder.setIcon(icon); + } + + // Build the message + final String content = message.getContent().getBody() == null ? "" : message.getContent().getBody(); + final NotificationCompat.MessagingStyle.Message msg = new NotificationCompat.MessagingStyle.Message( + content, + message.getTimestamp(), + personBuilder.build() + ); + // Turn the image path to a content Uri, if a media file was specified + if (message.getContent().getMime() != null && message.getContent().getPath() != null) { + final Uri fileUri = androidx.core.content.FileProvider.getUriForFile(context, "me.polynom.moxplatform_android.fileprovider", new File(message.getContent().getPath())); + msg.setData(message.getContent().getMime(), fileUri); + } + + style.addMessage(msg); + } + + // Build the notification and send it + final NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context, notification.getChannelId()) + .setStyle(style) + // TODO: This is wrong + .setSmallIcon(R.drawable.ic_service_icon) + .addAction(action) + .addAction(R.drawable.ic_service_icon, "Mark as read", markAsReadPendingIntent); + NotificationManagerCompat.from(context).notify(notification.getId().intValue(), notificationBuilder.build()); + } } 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 new file mode 100644 index 0000000..fa38e2d --- /dev/null +++ b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/NotificationReceiver.kt @@ -0,0 +1,28 @@ +package me.polynom.moxplatform_android + +import android.app.NotificationManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.RemoteInput + +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("title")} as read") + NotificationManagerCompat.from(context).cancel(intent.getLongExtra(MARK_AS_READ_ID_KEY, -1).toInt()) + return + } + + val remoteInput = RemoteInput.getResultsFromIntent(intent) ?: return + + val title = remoteInput.getCharSequence(REPLY_TEXT_KEY).toString() + Log.d("NotificationReceiver", title) + // TODO: Notify app + } +} \ No newline at end of file diff --git a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Notifications.java b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Notifications.java new file mode 100644 index 0000000..4bab733 --- /dev/null +++ b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Notifications.java @@ -0,0 +1,520 @@ +// Autogenerated from Pigeon (v10.1.4), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +package me.polynom.moxplatform_android; + +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.plugin.common.BasicMessageChannel; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MessageCodec; +import io.flutter.plugin.common.StandardMessageCodec; +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** Generated class from Pigeon. */ +@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"}) +public class Notifications { + + /** Error class for passing custom error details to Flutter via a thrown PlatformException. */ + public static class FlutterError extends RuntimeException { + + /** The error code. */ + public final String code; + + /** The error details. Must be a datatype supported by the api codec. */ + public final Object details; + + public FlutterError(@NonNull String code, @Nullable String message, @Nullable Object details) + { + super(message); + this.code = code; + this.details = details; + } + } + + @NonNull + protected static ArrayList wrapError(@NonNull Throwable exception) { + ArrayList errorList = new ArrayList(3); + if (exception instanceof FlutterError) { + FlutterError error = (FlutterError) exception; + errorList.add(error.code); + errorList.add(error.getMessage()); + errorList.add(error.details); + } else { + errorList.add(exception.toString()); + errorList.add(exception.getClass().getSimpleName()); + errorList.add( + "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); + } + return errorList; + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static final class NotificationMessageContent { + /** The textual body of the message. */ + private @Nullable String body; + + public @Nullable String getBody() { + return body; + } + + public void setBody(@Nullable String setterArg) { + this.body = setterArg; + } + + /** The path and mime type of the media to show. */ + private @Nullable String mime; + + public @Nullable String getMime() { + return mime; + } + + public void setMime(@Nullable String setterArg) { + this.mime = setterArg; + } + + private @Nullable String path; + + public @Nullable String getPath() { + return path; + } + + public void setPath(@Nullable String setterArg) { + this.path = setterArg; + } + + public static final class Builder { + + private @Nullable String body; + + public @NonNull Builder setBody(@Nullable String setterArg) { + this.body = setterArg; + return this; + } + + private @Nullable String mime; + + public @NonNull Builder setMime(@Nullable String setterArg) { + this.mime = setterArg; + return this; + } + + private @Nullable String path; + + public @NonNull Builder setPath(@Nullable String setterArg) { + this.path = setterArg; + return this; + } + + public @NonNull NotificationMessageContent build() { + NotificationMessageContent pigeonReturn = new NotificationMessageContent(); + pigeonReturn.setBody(body); + pigeonReturn.setMime(mime); + pigeonReturn.setPath(path); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(3); + toListResult.add(body); + toListResult.add(mime); + toListResult.add(path); + return toListResult; + } + + static @NonNull NotificationMessageContent fromList(@NonNull ArrayList list) { + NotificationMessageContent pigeonResult = new NotificationMessageContent(); + Object body = list.get(0); + pigeonResult.setBody((String) body); + Object mime = list.get(1); + pigeonResult.setMime((String) mime); + Object path = list.get(2); + pigeonResult.setPath((String) path); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static final class NotificationMessage { + /** The sender of the message. */ + private @NonNull String sender; + + public @NonNull String getSender() { + return sender; + } + + public void setSender(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"sender\" is null."); + } + this.sender = setterArg; + } + + /** The jid of the sender. */ + 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 body of the message. */ + private @NonNull NotificationMessageContent content; + + public @NonNull NotificationMessageContent getContent() { + return content; + } + + public void setContent(@NonNull NotificationMessageContent setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"content\" is null."); + } + this.content = setterArg; + } + + /** Milliseconds since epoch. */ + private @NonNull Long timestamp; + + public @NonNull Long getTimestamp() { + return timestamp; + } + + public void setTimestamp(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"timestamp\" is null."); + } + this.timestamp = setterArg; + } + + /** The path to the avatar to use */ + private @Nullable String avatarPath; + + public @Nullable String getAvatarPath() { + return avatarPath; + } + + public void setAvatarPath(@Nullable String setterArg) { + this.avatarPath = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + NotificationMessage() {} + + public static final class Builder { + + private @Nullable String sender; + + public @NonNull Builder setSender(@NonNull String setterArg) { + this.sender = setterArg; + return this; + } + + private @Nullable String jid; + + public @NonNull Builder setJid(@NonNull String setterArg) { + this.jid = setterArg; + return this; + } + + private @Nullable NotificationMessageContent content; + + public @NonNull Builder setContent(@NonNull NotificationMessageContent setterArg) { + this.content = setterArg; + return this; + } + + private @Nullable Long timestamp; + + public @NonNull Builder setTimestamp(@NonNull Long setterArg) { + this.timestamp = setterArg; + return this; + } + + private @Nullable String avatarPath; + + public @NonNull Builder setAvatarPath(@Nullable String setterArg) { + this.avatarPath = setterArg; + return this; + } + + public @NonNull NotificationMessage build() { + NotificationMessage pigeonReturn = new NotificationMessage(); + pigeonReturn.setSender(sender); + pigeonReturn.setJid(jid); + pigeonReturn.setContent(content); + pigeonReturn.setTimestamp(timestamp); + pigeonReturn.setAvatarPath(avatarPath); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(5); + toListResult.add(sender); + toListResult.add(jid); + toListResult.add((content == null) ? null : content.toList()); + toListResult.add(timestamp); + toListResult.add(avatarPath); + return toListResult; + } + + static @NonNull NotificationMessage fromList(@NonNull ArrayList list) { + NotificationMessage pigeonResult = new NotificationMessage(); + Object sender = list.get(0); + pigeonResult.setSender((String) sender); + Object jid = list.get(1); + pigeonResult.setJid((String) jid); + Object content = list.get(2); + pigeonResult.setContent((content == null) ? null : NotificationMessageContent.fromList((ArrayList) content)); + Object timestamp = list.get(3); + pigeonResult.setTimestamp((timestamp == null) ? null : ((timestamp instanceof Integer) ? (Integer) timestamp : (Long) timestamp)); + Object avatarPath = list.get(4); + pigeonResult.setAvatarPath((String) avatarPath); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static final class MessagingNotification { + /** The title of the conversation. */ + private @NonNull String title; + + public @NonNull String getTitle() { + return title; + } + + public void setTitle(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"title\" is null."); + } + this.title = setterArg; + } + + /** The id of the notification. */ + private @NonNull Long id; + + public @NonNull Long getId() { + return id; + } + + public void setId(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"id\" is null."); + } + this.id = setterArg; + } + + /** The id of the notification channel the notification should appear on. */ + private @NonNull String channelId; + + public @NonNull String getChannelId() { + return channelId; + } + + public void setChannelId(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"channelId\" is null."); + } + this.channelId = setterArg; + } + + /** Messages to show. */ + private @NonNull List messages; + + public @NonNull List getMessages() { + return messages; + } + + public void setMessages(@NonNull List setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"messages\" is null."); + } + this.messages = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + MessagingNotification() {} + + public static final class Builder { + + private @Nullable String title; + + public @NonNull Builder setTitle(@NonNull String setterArg) { + this.title = setterArg; + return this; + } + + private @Nullable Long id; + + public @NonNull Builder setId(@NonNull Long setterArg) { + this.id = setterArg; + return this; + } + + private @Nullable String channelId; + + public @NonNull Builder setChannelId(@NonNull String setterArg) { + this.channelId = setterArg; + return this; + } + + private @Nullable List messages; + + public @NonNull Builder setMessages(@NonNull List setterArg) { + this.messages = setterArg; + return this; + } + + public @NonNull MessagingNotification build() { + MessagingNotification pigeonReturn = new MessagingNotification(); + pigeonReturn.setTitle(title); + pigeonReturn.setId(id); + pigeonReturn.setChannelId(channelId); + pigeonReturn.setMessages(messages); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(4); + toListResult.add(title); + toListResult.add(id); + toListResult.add(channelId); + toListResult.add(messages); + return toListResult; + } + + static @NonNull MessagingNotification fromList(@NonNull ArrayList list) { + MessagingNotification pigeonResult = new MessagingNotification(); + Object title = list.get(0); + pigeonResult.setTitle((String) title); + Object id = list.get(1); + pigeonResult.setId((id == null) ? null : ((id instanceof Integer) ? (Integer) id : (Long) id)); + Object channelId = list.get(2); + pigeonResult.setChannelId((String) channelId); + Object messages = list.get(3); + pigeonResult.setMessages((List) messages); + return pigeonResult; + } + } + + private static class NotificationsImplementationApiCodec extends StandardMessageCodec { + public static final NotificationsImplementationApiCodec INSTANCE = new NotificationsImplementationApiCodec(); + + private NotificationsImplementationApiCodec() {} + + @Override + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { + switch (type) { + case (byte) 128: + return MessagingNotification.fromList((ArrayList) readValue(buffer)); + case (byte) 129: + return NotificationMessage.fromList((ArrayList) readValue(buffer)); + case (byte) 130: + return NotificationMessageContent.fromList((ArrayList) readValue(buffer)); + default: + return super.readValueOfType(type, buffer); + } + } + + @Override + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { + if (value instanceof MessagingNotification) { + stream.write(128); + writeValue(stream, ((MessagingNotification) value).toList()); + } else if (value instanceof NotificationMessage) { + stream.write(129); + writeValue(stream, ((NotificationMessage) value).toList()); + } else if (value instanceof NotificationMessageContent) { + stream.write(130); + writeValue(stream, ((NotificationMessageContent) value).toList()); + } else { + super.writeValue(stream, value); + } + } + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface NotificationsImplementationApi { + + void createNotificationChannel(@NonNull String title, @NonNull String id, @NonNull Boolean urgent); + + void showMessagingNotification(@NonNull MessagingNotification notification); + + /** The codec used by NotificationsImplementationApi. */ + static @NonNull MessageCodec getCodec() { + return NotificationsImplementationApiCodec.INSTANCE; + } + /**Sets up an instance of `NotificationsImplementationApi` to handle messages through the `binaryMessenger`. */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable NotificationsImplementationApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.moxplatform_platform_interface.NotificationsImplementationApi.createNotificationChannel", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + String titleArg = (String) args.get(0); + String idArg = (String) args.get(1); + Boolean urgentArg = (Boolean) args.get(2); + try { + api.createNotificationChannel(titleArg, idArg, urgentArg); + wrapped.add(0, null); + } + 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.NotificationsImplementationApi.showMessagingNotification", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + MessagingNotification notificationArg = (MessagingNotification) args.get(0); + try { + api.showMessagingNotification(notificationArg); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } +} diff --git a/packages/moxplatform_android/android/src/main/res/xml/file_paths.xml b/packages/moxplatform_android/android/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..030c2d3 --- /dev/null +++ b/packages/moxplatform_android/android/src/main/res/xml/file_paths.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/moxplatform_android/lib/src/notifications_android.dart b/packages/moxplatform_android/lib/src/notifications_android.dart new file mode 100644 index 0000000..98d00f9 --- /dev/null +++ b/packages/moxplatform_android/lib/src/notifications_android.dart @@ -0,0 +1,17 @@ +import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart'; + +class AndroidNotificationsImplementation extends NotificationsImplementation { + final NotificationsImplementationApi _api = NotificationsImplementationApi(); + + + @override + Future createNotificationChannel(String title, String id, bool urgent) async { + return _api.createNotificationChannel(title, id, urgent); + } + + + @override + Future showMessagingNotification(MessagingNotification notification) async { + return _api.showMessagingNotification(notification); + } +} diff --git a/packages/moxplatform_android/lib/src/plugin_android.dart b/packages/moxplatform_android/lib/src/plugin_android.dart index a024c23..087019e 100644 --- a/packages/moxplatform_android/lib/src/plugin_android.dart +++ b/packages/moxplatform_android/lib/src/plugin_android.dart @@ -2,6 +2,7 @@ import 'package:moxplatform_android/src/contacts_android.dart'; import 'package:moxplatform_android/src/crypto_android.dart'; import 'package:moxplatform_android/src/isolate_android.dart'; import 'package:moxplatform_android/src/media_android.dart'; +import 'package:moxplatform_android/src/notifications_android.dart'; import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart'; class MoxplatformAndroidPlugin extends MoxplatformInterface { @@ -12,6 +13,7 @@ class MoxplatformAndroidPlugin extends MoxplatformInterface { MoxplatformInterface.crypto = AndroidCryptographyImplementation(); MoxplatformInterface.handler = AndroidIsolateHandler(); MoxplatformInterface.media = AndroidMediaScannerImplementation(); + MoxplatformInterface.notifications = AndroidNotificationsImplementation(); } @override diff --git a/packages/moxplatform_android/pubspec.yaml b/packages/moxplatform_android/pubspec.yaml index 79fcd79..8151476 100644 --- a/packages/moxplatform_android/pubspec.yaml +++ b/packages/moxplatform_android/pubspec.yaml @@ -41,4 +41,5 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + pigeon: 10.1.4 very_good_analysis: ^3.0.1 diff --git a/packages/moxplatform_platform_interface/lib/moxplatform_platform_interface.dart b/packages/moxplatform_platform_interface/lib/moxplatform_platform_interface.dart index 9b2a14a..7f1d835 100644 --- a/packages/moxplatform_platform_interface/lib/moxplatform_platform_interface.dart +++ b/packages/moxplatform_platform_interface/lib/moxplatform_platform_interface.dart @@ -9,4 +9,7 @@ export 'src/isolate.dart'; export 'src/isolate_stub.dart'; export 'src/media.dart'; export 'src/media_stub.dart'; +export 'src/notifications.dart'; +export 'src/notifications.g.dart'; +export 'src/notifications_stub.dart'; export 'src/service.dart'; diff --git a/packages/moxplatform_platform_interface/lib/src/interface.dart b/packages/moxplatform_platform_interface/lib/src/interface.dart index 6005fb7..cbc180c 100644 --- a/packages/moxplatform_platform_interface/lib/src/interface.dart +++ b/packages/moxplatform_platform_interface/lib/src/interface.dart @@ -6,6 +6,8 @@ import 'package:moxplatform_platform_interface/src/isolate.dart'; import 'package:moxplatform_platform_interface/src/isolate_stub.dart'; import 'package:moxplatform_platform_interface/src/media.dart'; import 'package:moxplatform_platform_interface/src/media_stub.dart'; +import 'package:moxplatform_platform_interface/src/notifications.dart'; +import 'package:moxplatform_platform_interface/src/notifications_stub.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; abstract class MoxplatformInterface extends PlatformInterface { @@ -17,6 +19,7 @@ abstract class MoxplatformInterface extends PlatformInterface { static MediaScannerImplementation media = StubMediaScannerImplementation(); static CryptographyImplementation crypto = StubCryptographyImplementation(); static ContactsImplementation contacts = StubContactsImplementation(); + static NotificationsImplementation notifications = StubNotificationsImplementation(); /// Return the current platform name. Future getPlatformName(); diff --git a/packages/moxplatform_platform_interface/lib/src/notifications.dart b/packages/moxplatform_platform_interface/lib/src/notifications.dart new file mode 100644 index 0000000..6fe132a --- /dev/null +++ b/packages/moxplatform_platform_interface/lib/src/notifications.dart @@ -0,0 +1,7 @@ +import 'package:moxplatform_platform_interface/src/notifications.g.dart'; + +abstract class NotificationsImplementation { + Future createNotificationChannel(String title, String id, bool urgent); + + Future showMessagingNotification(MessagingNotification notification); +} diff --git a/packages/moxplatform_platform_interface/lib/src/notifications.g.dart b/packages/moxplatform_platform_interface/lib/src/notifications.g.dart new file mode 100644 index 0000000..0200d46 --- /dev/null +++ b/packages/moxplatform_platform_interface/lib/src/notifications.g.dart @@ -0,0 +1,216 @@ +// Autogenerated from Pigeon (v10.1.4), 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 NotificationMessageContent { + NotificationMessageContent({ + this.body, + this.mime, + this.path, + }); + + /// The textual body of the message. + String? body; + + /// The path and mime type of the media to show. + String? mime; + + String? path; + + Object encode() { + return [ + body, + mime, + path, + ]; + } + + static NotificationMessageContent decode(Object result) { + result as List; + return NotificationMessageContent( + body: result[0] as String?, + mime: result[1] as String?, + path: result[2] as String?, + ); + } +} + +class NotificationMessage { + NotificationMessage({ + required this.sender, + required this.jid, + required this.content, + required this.timestamp, + this.avatarPath, + }); + + /// The sender of the message. + String sender; + + /// The jid of the sender. + String jid; + + /// The body of the message. + NotificationMessageContent content; + + /// Milliseconds since epoch. + int timestamp; + + /// The path to the avatar to use + String? avatarPath; + + Object encode() { + return [ + sender, + jid, + content.encode(), + timestamp, + avatarPath, + ]; + } + + static NotificationMessage decode(Object result) { + result as List; + return NotificationMessage( + sender: result[0]! as String, + jid: result[1]! as String, + content: NotificationMessageContent.decode(result[2]! as List), + timestamp: result[3]! as int, + avatarPath: result[4] as String?, + ); + } +} + +class MessagingNotification { + MessagingNotification({ + required this.title, + required this.id, + required this.channelId, + required this.messages, + }); + + /// The title of the conversation. + String title; + + /// The id of the notification. + int id; + + /// The id of the notification channel the notification should appear on. + String channelId; + + /// Messages to show. + List messages; + + Object encode() { + return [ + title, + id, + channelId, + messages, + ]; + } + + static MessagingNotification decode(Object result) { + result as List; + return MessagingNotification( + title: result[0]! as String, + id: result[1]! as int, + channelId: result[2]! as String, + messages: (result[3] as List?)!.cast(), + ); + } +} + +class _NotificationsImplementationApiCodec extends StandardMessageCodec { + const _NotificationsImplementationApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is MessagingNotification) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is NotificationMessage) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is NotificationMessageContent) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return MessagingNotification.decode(readValue(buffer)!); + case 129: + return NotificationMessage.decode(readValue(buffer)!); + case 130: + return NotificationMessageContent.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +class NotificationsImplementationApi { + /// Constructor for [NotificationsImplementationApi]. 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. + NotificationsImplementationApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _NotificationsImplementationApiCodec(); + + Future createNotificationChannel(String arg_title, String arg_id, bool arg_urgent) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.moxplatform_platform_interface.NotificationsImplementationApi.createNotificationChannel', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_title, arg_id, arg_urgent]) 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 showMessagingNotification(MessagingNotification arg_notification) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.moxplatform_platform_interface.NotificationsImplementationApi.showMessagingNotification', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_notification]) 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_stub.dart b/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart new file mode 100644 index 0000000..fef3f65 --- /dev/null +++ b/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart @@ -0,0 +1,10 @@ +import 'package:moxplatform_platform_interface/src/notifications.g.dart'; +import 'package:moxplatform_platform_interface/src/notifications.dart'; + +class StubNotificationsImplementation extends NotificationsImplementation { + @override + Future createNotificationChannel(String title, String id, bool urgent) async {} + + @override + Future showMessagingNotification(MessagingNotification notification) async {} +} diff --git a/packages/moxplatform_platform_interface/packages/moxplatform_android/lib/src/contacts_android.dart b/packages/moxplatform_platform_interface/packages/moxplatform_android/lib/src/contacts_android.dart deleted file mode 100644 index e69de29..0000000 diff --git a/packages/moxplatform_platform_interface/packages/moxplatform_platform_interface/lib/src/contacts.dart b/packages/moxplatform_platform_interface/packages/moxplatform_platform_interface/lib/src/contacts.dart deleted file mode 100644 index e69de29..0000000 diff --git a/packages/moxplatform_platform_interface/packages/moxplatform_platform_interface/lib/src/contacts_stub.dart b/packages/moxplatform_platform_interface/packages/moxplatform_platform_interface/lib/src/contacts_stub.dart deleted file mode 100644 index e69de29..0000000 diff --git a/pigeons/notifications.dart b/pigeons/notifications.dart new file mode 100644 index 0000000..1aa1c54 --- /dev/null +++ b/pigeons/notifications.dart @@ -0,0 +1,77 @@ +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon( + PigeonOptions( + dartOut: 'packages/moxplatform_platform_interface/lib/src/notifications.g.dart', + //kotlinOut: 'packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Notifications.g.kt', + //kotlinOptions: KotlinOptions( + // package: 'me.polynom.moxplatform_android', + //), + javaOut: 'packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Notifications.java', + javaOptions: JavaOptions( + package: 'me.polynom.moxplatform_android', + ), + ), +) +class NotificationMessageContent { + const NotificationMessageContent( + this.body, + this.mime, + this.path, + ); + + /// The textual body of the message. + final String? body; + + /// The path and mime type of the media to show. + final String? mime; + final String? path; +} + +class NotificationMessage { + const NotificationMessage( + this.sender, + this.content, + this.jid, + this.timestamp, + this.avatarPath, + ); + + /// The sender of the message. + final String sender; + + /// The jid of the sender. + final String jid; + + /// The body of the message. + final NotificationMessageContent content; + + /// Milliseconds since epoch. + final int timestamp; + + /// The path to the avatar to use + final String? avatarPath; +} + +class MessagingNotification { + const MessagingNotification(this.title, this.id, this.messages, this.channelId); + + /// The title of the conversation. + final String title; + + /// The id of the notification. + final int id; + + /// The id of the notification channel the notification should appear on. + final String channelId; + + /// Messages to show. + final List messages; +} + +@HostApi() +abstract class NotificationsImplementationApi { + void createNotificationChannel(String title, String id, bool urgent); + + void showMessagingNotification(MessagingNotification notification); +} diff --git a/pubspec.yaml b/pubspec.yaml index eee1b21..9bfc27f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,3 +4,4 @@ environment: sdk: '>=2.18.0 <3.0.0' dev_dependencies: melos: ^3.1.1 + pigeon: 10.1.4 From 864b868f456d6fabbd6967e7c1d7f076648a59c8 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Fri, 28 Jul 2023 00:46:19 +0200 Subject: [PATCH 02/23] Rewrite the notification code in Kotlin --- example/lib/main.dart | 12 ++ packages/moxplatform/lib/src/plugin.dart | 2 +- .../lib/src/platform.dart | 0 .../lib/src/platform_stub.dart | 0 .../{Notifications.java => Api.java} | 72 +++++++++-- .../MoxplatformAndroidPlugin.java | 86 +++---------- .../moxplatform_android/Notifications.kt | 115 ++++++++++++++++++ .../android/src/main/res/xml/file_paths.xml | 3 + .../lib/moxplatform_android.dart | 1 - .../lib/src/media_android.dart | 9 -- .../lib/src/notifications_android.dart | 2 +- .../lib/src/platform_android.dart | 13 ++ .../lib/src/plugin_android.dart | 4 +- .../lib/src/platform_android.dart | 0 packages/moxplatform_android/pubspec.yaml | 1 - .../lib/moxplatform_platform_interface.dart | 6 +- .../src/{notifications.g.dart => api.g.dart} | 70 +++++++++-- .../lib/src/interface.dart | 9 +- .../lib/src/media.dart | 6 - .../lib/src/media_stub.dart | 6 - .../lib/src/notifications.dart | 2 +- .../lib/src/notifications_stub.dart | 2 +- .../lib/src/platform.dart | 7 ++ .../lib/src/platform_stub.dart | 9 ++ pigeons/notifications.dart | 10 +- 25 files changed, 318 insertions(+), 129 deletions(-) create mode 100644 packages/moxplatform/packages/moxplatform_platform_interface/lib/src/platform.dart create mode 100644 packages/moxplatform/packages/moxplatform_platform_interface/lib/src/platform_stub.dart rename packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/{Notifications.java => Api.java} (86%) create mode 100644 packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Notifications.kt delete mode 100644 packages/moxplatform_android/lib/src/media_android.dart create mode 100644 packages/moxplatform_android/lib/src/platform_android.dart create mode 100644 packages/moxplatform_android/packages/moxplatform_android/lib/src/platform_android.dart rename packages/moxplatform_platform_interface/lib/src/{notifications.g.dart => api.g.dart} (69%) delete mode 100644 packages/moxplatform_platform_interface/lib/src/media.dart delete mode 100644 packages/moxplatform_platform_interface/lib/src/media_stub.dart create mode 100644 packages/moxplatform_platform_interface/lib/src/platform.dart create mode 100644 packages/moxplatform_platform_interface/lib/src/platform_stub.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index eeeb251..190aa62 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -175,6 +175,18 @@ class MyHomePage extends StatelessWidget { }, child: const Text('Show messaging notification'), ), + ElevatedButton( + onPressed: () async { + print(await MoxplatformPlugin.platform.getPersistentDataPath()); + }, + child: const Text('Get data directory'), + ), + ElevatedButton( + onPressed: () async { + print(await MoxplatformPlugin.platform.getCacheDataPath()); + }, + child: const Text('Get cache directory'), + ), ], ), ), diff --git a/packages/moxplatform/lib/src/plugin.dart b/packages/moxplatform/lib/src/plugin.dart index bc3d6c1..39853c2 100644 --- a/packages/moxplatform/lib/src/plugin.dart +++ b/packages/moxplatform/lib/src/plugin.dart @@ -2,8 +2,8 @@ import 'package:moxplatform_platform_interface/moxplatform_platform_interface.da class MoxplatformPlugin { static IsolateHandler get handler => MoxplatformInterface.handler; - static MediaScannerImplementation get media => MoxplatformInterface.media; static CryptographyImplementation get crypto => MoxplatformInterface.crypto; static ContactsImplementation get contacts => MoxplatformInterface.contacts; static NotificationsImplementation get notifications => MoxplatformInterface.notifications; + static PlatformImplementation get platform => MoxplatformInterface.platform; } diff --git a/packages/moxplatform/packages/moxplatform_platform_interface/lib/src/platform.dart b/packages/moxplatform/packages/moxplatform_platform_interface/lib/src/platform.dart new file mode 100644 index 0000000..e69de29 diff --git a/packages/moxplatform/packages/moxplatform_platform_interface/lib/src/platform_stub.dart b/packages/moxplatform/packages/moxplatform_platform_interface/lib/src/platform_stub.dart new file mode 100644 index 0000000..e69de29 diff --git a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Notifications.java b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Api.java similarity index 86% rename from packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Notifications.java rename to packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Api.java index 4bab733..beb1e1a 100644 --- a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Notifications.java +++ b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Api.java @@ -21,7 +21,7 @@ import java.util.Map; /** Generated class from Pigeon. */ @SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"}) -public class Notifications { +public class Api { /** Error class for passing custom error details to Flutter via a thrown PlatformException. */ public static class FlutterError extends RuntimeException { @@ -416,10 +416,10 @@ public class Notifications { } } - private static class NotificationsImplementationApiCodec extends StandardMessageCodec { - public static final NotificationsImplementationApiCodec INSTANCE = new NotificationsImplementationApiCodec(); + private static class MoxplatformApiCodec extends StandardMessageCodec { + public static final MoxplatformApiCodec INSTANCE = new MoxplatformApiCodec(); - private NotificationsImplementationApiCodec() {} + private MoxplatformApiCodec() {} @Override protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { @@ -453,22 +453,28 @@ public class Notifications { } /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ - public interface NotificationsImplementationApi { + public interface MoxplatformApi { void createNotificationChannel(@NonNull String title, @NonNull String id, @NonNull Boolean urgent); void showMessagingNotification(@NonNull MessagingNotification notification); - /** The codec used by NotificationsImplementationApi. */ + @NonNull + String getPersistentDataPath(); + + @NonNull + String getCacheDataPath(); + + /** The codec used by MoxplatformApi. */ static @NonNull MessageCodec getCodec() { - return NotificationsImplementationApiCodec.INSTANCE; + return MoxplatformApiCodec.INSTANCE; } - /**Sets up an instance of `NotificationsImplementationApi` to handle messages through the `binaryMessenger`. */ - static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable NotificationsImplementationApi api) { + /**Sets up an instance of `MoxplatformApi` to handle messages through the `binaryMessenger`. */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable MoxplatformApi api) { { BasicMessageChannel channel = new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.moxplatform_platform_interface.NotificationsImplementationApi.createNotificationChannel", getCodec()); + binaryMessenger, "dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.createNotificationChannel", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { @@ -494,7 +500,7 @@ public class Notifications { { BasicMessageChannel channel = new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.moxplatform_platform_interface.NotificationsImplementationApi.showMessagingNotification", getCodec()); + binaryMessenger, "dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.showMessagingNotification", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { @@ -505,6 +511,50 @@ public class Notifications { api.showMessagingNotification(notificationArg); wrapped.add(0, null); } + 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.getPersistentDataPath", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + String output = api.getPersistentDataPath(); + 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.getCacheDataPath", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + String output = api.getCacheDataPath(); + wrapped.add(0, output); + } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; 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 c8fb6d8..0515d6b 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 @@ -1,36 +1,24 @@ package me.polynom.moxplatform_android; import static androidx.core.content.ContextCompat.getSystemService; -import static me.polynom.moxplatform_android.ConstantsKt.MARK_AS_READ_ACTION; -import static me.polynom.moxplatform_android.ConstantsKt.MARK_AS_READ_ID_KEY; -import static me.polynom.moxplatform_android.ConstantsKt.REPLY_TEXT_KEY; import static me.polynom.moxplatform_android.RecordSentMessageKt.recordSentMessage; import static me.polynom.moxplatform_android.CryptoKt.*; -import me.polynom.moxplatform_android.Notifications.*; +import me.polynom.moxplatform_android.Api.*; import android.app.ActivityManager; import android.app.NotificationChannel; import android.app.NotificationManager; -import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; -import android.graphics.BitmapFactory; -import android.net.Uri; import android.util.Log; import androidx.annotation.NonNull; -import androidx.core.app.RemoteInput; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; -import androidx.core.app.Person; import androidx.core.content.ContextCompat; -import androidx.core.graphics.drawable.IconCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import java.io.File; import java.util.ArrayList; import java.util.List; @@ -46,7 +34,7 @@ import io.flutter.plugin.common.JSONMethodCodec; import kotlin.Unit; import kotlin.jvm.functions.Function1; -public class MoxplatformAndroidPlugin extends BroadcastReceiver implements FlutterPlugin, MethodCallHandler, ServiceAware, NotificationsImplementationApi { +public class MoxplatformAndroidPlugin extends BroadcastReceiver implements FlutterPlugin, MethodCallHandler, 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"; @@ -60,8 +48,6 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt private MethodChannel channel; private Context context; - private FileProvider provider = new FileProvider(); - public MoxplatformAndroidPlugin() { _instances.add(this); } @@ -75,7 +61,7 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(this.context); localBroadcastManager.registerReceiver(this, new IntentFilter(methodChannelKey)); - NotificationsImplementationApi.setup(flutterPluginBinding.getBinaryMessenger(), this); + MoxplatformApi.setup(flutterPluginBinding.getBinaryMessenger(), this); Log.d(TAG, "Attached to engine"); } @@ -292,62 +278,18 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt @Override public void showMessagingNotification(@NonNull MessagingNotification notification) { - // Create a reply button - // TODO: i18n - RemoteInput remoteInput = new RemoteInput.Builder(REPLY_TEXT_KEY).setLabel("Reply").build(); - final Intent replyIntent = new Intent(context, NotificationReceiver.class); - final PendingIntent replyPendingIntent = PendingIntent.getBroadcast(context.getApplicationContext(), 0, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT); - // TODO: i18n - // TODO: Correct icon - final NotificationCompat.Action action = new NotificationCompat.Action.Builder(R.drawable.ic_service_icon, "Reply", replyPendingIntent) - .addRemoteInput(remoteInput) - .build(); + NotificationsKt.showMessagingNotification(context, notification); + } - // Create the "mark as read" button - final Intent markAsReadIntent = new Intent(context, NotificationReceiver.class); - markAsReadIntent.setAction(MARK_AS_READ_ACTION); - markAsReadIntent.putExtra(MARK_AS_READ_ID_KEY, notification.getId()); - // TODO: Replace with something more useful - markAsReadIntent.putExtra("title", notification.getTitle()); - final PendingIntent markAsReadPendingIntent = PendingIntent.getBroadcast(context.getApplicationContext(), 0, readIntent,PendingIntent.FLAG_CANCEL_CURRENT); + @NonNull + @Override + public String getPersistentDataPath() { + return context.getFilesDir().getPath(); + } - final NotificationCompat.MessagingStyle style = new NotificationCompat.MessagingStyle("Me") - .setConversationTitle(notification.getTitle()); - for (final NotificationMessage message : notification.getMessages()) { - // Build the sender of the message - final Person.Builder personBuilder = new Person.Builder() - .setName(message.getSender()) - .setKey(message.getJid()); - if (message.getAvatarPath() != null) { - final IconCompat icon = IconCompat.createWithAdaptiveBitmap( - BitmapFactory.decodeFile(message.getAvatarPath()) - ); - personBuilder.setIcon(icon); - } - - // Build the message - final String content = message.getContent().getBody() == null ? "" : message.getContent().getBody(); - final NotificationCompat.MessagingStyle.Message msg = new NotificationCompat.MessagingStyle.Message( - content, - message.getTimestamp(), - personBuilder.build() - ); - // Turn the image path to a content Uri, if a media file was specified - if (message.getContent().getMime() != null && message.getContent().getPath() != null) { - final Uri fileUri = androidx.core.content.FileProvider.getUriForFile(context, "me.polynom.moxplatform_android.fileprovider", new File(message.getContent().getPath())); - msg.setData(message.getContent().getMime(), fileUri); - } - - style.addMessage(msg); - } - - // Build the notification and send it - final NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context, notification.getChannelId()) - .setStyle(style) - // TODO: This is wrong - .setSmallIcon(R.drawable.ic_service_icon) - .addAction(action) - .addAction(R.drawable.ic_service_icon, "Mark as read", markAsReadPendingIntent); - NotificationManagerCompat.from(context).notify(notification.getId().intValue(), notificationBuilder.build()); + @NonNull + @Override + public String getCacheDataPath() { + return context.getCacheDir().getPath(); } } 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 new file mode 100644 index 0000000..907e233 --- /dev/null +++ b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Notifications.kt @@ -0,0 +1,115 @@ +package me.polynom.moxplatform_android + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.graphics.BitmapFactory +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.Person +import androidx.core.app.RemoteInput +import androidx.core.content.FileProvider +import androidx.core.graphics.drawable.IconCompat +import java.io.File + +/// Show a messaging style notification described by @notification. +fun showMessagingNotification(context: Context, notification: Api.MessagingNotification) { + // Build the actions + // -> Reply action + val remoteInput = RemoteInput.Builder(REPLY_TEXT_KEY).apply { + // TODO: i18n + setLabel("Reply") + }.build() + val replyIntent = Intent(context, NotificationReceiver::class.java) + val replyPendingIntent = PendingIntent.getBroadcast( + context.applicationContext, + 0, + replyIntent, + PendingIntent.FLAG_UPDATE_CURRENT, + ) + val replyAction = NotificationCompat.Action.Builder( + // TODO: Wrong icon? + R.drawable.ic_service_icon, + // TODO: i18n + "Reply", + replyPendingIntent, + ).apply { + addRemoteInput(remoteInput) + }.build() + + // -> Mark as read action + val markAsReadIntent = Intent(context, NotificationReceiver::class.java).apply { + action = MARK_AS_READ_ACTION + // TODO: Put the JID here + putExtra("title", notification.title) + } + val markAsReadPendingIntent = PendingIntent.getBroadcast( + context.applicationContext, + 0, + markAsReadIntent, + 0, + ) + + // Build the notification + // TODO: Use a person + // TODO: i18n + val style = NotificationCompat.MessagingStyle("Me"); + for (message in notification.messages) { + // Build the sender + val sender = Person.Builder().apply { + setName(message.sender) + setKey(message.jid) + + // Set the avatar, if available + if (message.avatarPath != null) { + setIcon( + IconCompat.createWithAdaptiveBitmap( + BitmapFactory.decodeFile(message.avatarPath), + ), + ) + } + }.build() + + // Build the message + val body = message.content.body ?: "" + val msg = NotificationCompat.MessagingStyle.Message( + body, + message.timestamp, + sender, + ) + // If we got an image, turn it into a content URI and set it + if (message.content.mime != null && message.content.path != null) { + val fileUri = FileProvider.getUriForFile( + context, + "me.polynom.moxplatform_android.fileprovider", + File(message.content.path), + ) + msg.setData(message.content.mime, fileUri) + } + + // Append the message + style.addMessage(msg) + } + + // Assemble the notification + val finalNotification = NotificationCompat.Builder(context, notification.channelId).apply { + setStyle(style) + // TODO: I think this is wrong + setSmallIcon(R.drawable.ic_service_icon) + + addAction(replyAction) + addAction( + // TODO: Wrong icon + R.drawable.ic_service_icon, + // TODO: i18n + "Mark as read", + markAsReadPendingIntent, + ) + }.build() + + // Post the notification + NotificationManagerCompat.from(context).notify( + notification.id.toInt(), + finalNotification, + ) +} \ No newline at end of file diff --git a/packages/moxplatform_android/android/src/main/res/xml/file_paths.xml b/packages/moxplatform_android/android/src/main/res/xml/file_paths.xml index 030c2d3..72b6cf6 100644 --- a/packages/moxplatform_android/android/src/main/res/xml/file_paths.xml +++ b/packages/moxplatform_android/android/src/main/res/xml/file_paths.xml @@ -1,4 +1,7 @@ + + + \ No newline at end of file diff --git a/packages/moxplatform_android/lib/moxplatform_android.dart b/packages/moxplatform_android/lib/moxplatform_android.dart index 8a55cf4..5167d95 100644 --- a/packages/moxplatform_android/lib/moxplatform_android.dart +++ b/packages/moxplatform_android/lib/moxplatform_android.dart @@ -1,6 +1,5 @@ library moxplatform_android; export 'src/isolate_android.dart'; -export 'src/media_android.dart'; export 'src/plugin_android.dart'; export 'src/service_android.dart'; diff --git a/packages/moxplatform_android/lib/src/media_android.dart b/packages/moxplatform_android/lib/src/media_android.dart deleted file mode 100644 index 88da65f..0000000 --- a/packages/moxplatform_android/lib/src/media_android.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:media_scanner/media_scanner.dart'; -import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart'; - -class AndroidMediaScannerImplementation extends MediaScannerImplementation { - @override - void scanFile(String path) { - MediaScanner.loadMedia(path: path); - } -} diff --git a/packages/moxplatform_android/lib/src/notifications_android.dart b/packages/moxplatform_android/lib/src/notifications_android.dart index 98d00f9..37246b2 100644 --- a/packages/moxplatform_android/lib/src/notifications_android.dart +++ b/packages/moxplatform_android/lib/src/notifications_android.dart @@ -1,7 +1,7 @@ import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart'; class AndroidNotificationsImplementation extends NotificationsImplementation { - final NotificationsImplementationApi _api = NotificationsImplementationApi(); + final MoxplatformApi _api = MoxplatformApi(); @override diff --git a/packages/moxplatform_android/lib/src/platform_android.dart b/packages/moxplatform_android/lib/src/platform_android.dart new file mode 100644 index 0000000..7d3c28e --- /dev/null +++ b/packages/moxplatform_android/lib/src/platform_android.dart @@ -0,0 +1,13 @@ +import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart'; + +class AndroidPlatformImplementation extends PlatformImplementation { + @override + Future getCacheDataPath() { + return MoxplatformInterface.api.getCacheDataPath(); + } + + @override + Future getPersistentDataPath() { + return MoxplatformInterface.api.getPersistentDataPath(); + } +} diff --git a/packages/moxplatform_android/lib/src/plugin_android.dart b/packages/moxplatform_android/lib/src/plugin_android.dart index 087019e..63a53a9 100644 --- a/packages/moxplatform_android/lib/src/plugin_android.dart +++ b/packages/moxplatform_android/lib/src/plugin_android.dart @@ -1,8 +1,8 @@ import 'package:moxplatform_android/src/contacts_android.dart'; import 'package:moxplatform_android/src/crypto_android.dart'; import 'package:moxplatform_android/src/isolate_android.dart'; -import 'package:moxplatform_android/src/media_android.dart'; import 'package:moxplatform_android/src/notifications_android.dart'; +import 'package:moxplatform_android/src/platform_android.dart'; import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart'; class MoxplatformAndroidPlugin extends MoxplatformInterface { @@ -12,8 +12,8 @@ class MoxplatformAndroidPlugin extends MoxplatformInterface { MoxplatformInterface.contacts = AndroidContactsImplementation(); MoxplatformInterface.crypto = AndroidCryptographyImplementation(); MoxplatformInterface.handler = AndroidIsolateHandler(); - MoxplatformInterface.media = AndroidMediaScannerImplementation(); MoxplatformInterface.notifications = AndroidNotificationsImplementation(); + MoxplatformInterface.platform = AndroidPlatformImplementation(); } @override diff --git a/packages/moxplatform_android/packages/moxplatform_android/lib/src/platform_android.dart b/packages/moxplatform_android/packages/moxplatform_android/lib/src/platform_android.dart new file mode 100644 index 0000000..e69de29 diff --git a/packages/moxplatform_android/pubspec.yaml b/packages/moxplatform_android/pubspec.yaml index 8151476..e9b12ff 100644 --- a/packages/moxplatform_android/pubspec.yaml +++ b/packages/moxplatform_android/pubspec.yaml @@ -22,7 +22,6 @@ dependencies: sdk: flutter get_it: ^7.2.0 logging: ^1.0.2 - media_scanner: ^2.0.0 meta: ^1.7.0 moxlib: hosted: https://git.polynom.me/api/packages/Moxxy/pub diff --git a/packages/moxplatform_platform_interface/lib/moxplatform_platform_interface.dart b/packages/moxplatform_platform_interface/lib/moxplatform_platform_interface.dart index 7f1d835..f391be5 100644 --- a/packages/moxplatform_platform_interface/lib/moxplatform_platform_interface.dart +++ b/packages/moxplatform_platform_interface/lib/moxplatform_platform_interface.dart @@ -1,5 +1,6 @@ library moxplatform_platform_interface; +export 'src/api.g.dart'; export 'src/contacts.dart'; export 'src/contacts_stub.dart'; export 'src/crypto.dart'; @@ -7,9 +8,8 @@ export 'src/crypto_stub.dart'; export 'src/interface.dart'; export 'src/isolate.dart'; export 'src/isolate_stub.dart'; -export 'src/media.dart'; -export 'src/media_stub.dart'; export 'src/notifications.dart'; -export 'src/notifications.g.dart'; export 'src/notifications_stub.dart'; +export 'src/platform.dart'; +export 'src/platform_stub.dart'; export 'src/service.dart'; diff --git a/packages/moxplatform_platform_interface/lib/src/notifications.g.dart b/packages/moxplatform_platform_interface/lib/src/api.g.dart similarity index 69% rename from packages/moxplatform_platform_interface/lib/src/notifications.g.dart rename to packages/moxplatform_platform_interface/lib/src/api.g.dart index 0200d46..09299ef 100644 --- a/packages/moxplatform_platform_interface/lib/src/notifications.g.dart +++ b/packages/moxplatform_platform_interface/lib/src/api.g.dart @@ -127,8 +127,8 @@ class MessagingNotification { } } -class _NotificationsImplementationApiCodec extends StandardMessageCodec { - const _NotificationsImplementationApiCodec(); +class _MoxplatformApiCodec extends StandardMessageCodec { + const _MoxplatformApiCodec(); @override void writeValue(WriteBuffer buffer, Object? value) { if (value is MessagingNotification) { @@ -160,19 +160,19 @@ class _NotificationsImplementationApiCodec extends StandardMessageCodec { } } -class NotificationsImplementationApi { - /// Constructor for [NotificationsImplementationApi]. The [binaryMessenger] named argument is +class MoxplatformApi { + /// Constructor for [MoxplatformApi]. 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. - NotificationsImplementationApi({BinaryMessenger? binaryMessenger}) + MoxplatformApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _NotificationsImplementationApiCodec(); + static const MessageCodec codec = _MoxplatformApiCodec(); Future createNotificationChannel(String arg_title, String arg_id, bool arg_urgent) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.moxplatform_platform_interface.NotificationsImplementationApi.createNotificationChannel', codec, + 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.createNotificationChannel', codec, binaryMessenger: _binaryMessenger); final List? replyList = await channel.send([arg_title, arg_id, arg_urgent]) as List?; @@ -194,7 +194,7 @@ class NotificationsImplementationApi { Future showMessagingNotification(MessagingNotification arg_notification) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.moxplatform_platform_interface.NotificationsImplementationApi.showMessagingNotification', codec, + 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.showMessagingNotification', codec, binaryMessenger: _binaryMessenger); final List? replyList = await channel.send([arg_notification]) as List?; @@ -213,4 +213,58 @@ class NotificationsImplementationApi { return; } } + + Future getPersistentDataPath() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.getPersistentDataPath', 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 getCacheDataPath() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.getCacheDataPath', 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?)!; + } + } } diff --git a/packages/moxplatform_platform_interface/lib/src/interface.dart b/packages/moxplatform_platform_interface/lib/src/interface.dart index cbc180c..45fd5d2 100644 --- a/packages/moxplatform_platform_interface/lib/src/interface.dart +++ b/packages/moxplatform_platform_interface/lib/src/interface.dart @@ -1,13 +1,14 @@ +import 'package:moxplatform_platform_interface/src/api.g.dart'; import 'package:moxplatform_platform_interface/src/contacts.dart'; import 'package:moxplatform_platform_interface/src/contacts_stub.dart'; import 'package:moxplatform_platform_interface/src/crypto.dart'; import 'package:moxplatform_platform_interface/src/crypto_stub.dart'; import 'package:moxplatform_platform_interface/src/isolate.dart'; import 'package:moxplatform_platform_interface/src/isolate_stub.dart'; -import 'package:moxplatform_platform_interface/src/media.dart'; -import 'package:moxplatform_platform_interface/src/media_stub.dart'; import 'package:moxplatform_platform_interface/src/notifications.dart'; import 'package:moxplatform_platform_interface/src/notifications_stub.dart'; +import 'package:moxplatform_platform_interface/src/platform.dart'; +import 'package:moxplatform_platform_interface/src/platform_stub.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; abstract class MoxplatformInterface extends PlatformInterface { @@ -15,11 +16,13 @@ abstract class MoxplatformInterface extends PlatformInterface { static final Object _token = Object(); + static MoxplatformApi api = MoxplatformApi(); + static IsolateHandler handler = StubIsolateHandler(); - static MediaScannerImplementation media = StubMediaScannerImplementation(); static CryptographyImplementation crypto = StubCryptographyImplementation(); static ContactsImplementation contacts = StubContactsImplementation(); static NotificationsImplementation notifications = StubNotificationsImplementation(); + static PlatformImplementation platform = StubPlatformImplementation(); /// Return the current platform name. Future getPlatformName(); diff --git a/packages/moxplatform_platform_interface/lib/src/media.dart b/packages/moxplatform_platform_interface/lib/src/media.dart deleted file mode 100644 index 23f7244..0000000 --- a/packages/moxplatform_platform_interface/lib/src/media.dart +++ /dev/null @@ -1,6 +0,0 @@ -/// Wrapper around platform-specific media scanning -// ignore: one_member_abstracts -abstract class MediaScannerImplementation { - /// Let the platform-specific media scanner scan the file at [path]. - void scanFile(String path); -} diff --git a/packages/moxplatform_platform_interface/lib/src/media_stub.dart b/packages/moxplatform_platform_interface/lib/src/media_stub.dart deleted file mode 100644 index 5ce84ba..0000000 --- a/packages/moxplatform_platform_interface/lib/src/media_stub.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:moxplatform_platform_interface/src/media.dart'; - -class StubMediaScannerImplementation extends MediaScannerImplementation { - @override - void scanFile(String path) {} -} diff --git a/packages/moxplatform_platform_interface/lib/src/notifications.dart b/packages/moxplatform_platform_interface/lib/src/notifications.dart index 6fe132a..d12cfb8 100644 --- a/packages/moxplatform_platform_interface/lib/src/notifications.dart +++ b/packages/moxplatform_platform_interface/lib/src/notifications.dart @@ -1,4 +1,4 @@ -import 'package:moxplatform_platform_interface/src/notifications.g.dart'; +import 'package:moxplatform_platform_interface/src/api.g.dart'; abstract class NotificationsImplementation { Future createNotificationChannel(String title, String id, bool urgent); diff --git a/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart b/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart index fef3f65..b912f10 100644 --- a/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart +++ b/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart @@ -1,4 +1,4 @@ -import 'package:moxplatform_platform_interface/src/notifications.g.dart'; +import 'package:moxplatform_platform_interface/src/api.g.dart'; import 'package:moxplatform_platform_interface/src/notifications.dart'; class StubNotificationsImplementation extends NotificationsImplementation { diff --git a/packages/moxplatform_platform_interface/lib/src/platform.dart b/packages/moxplatform_platform_interface/lib/src/platform.dart new file mode 100644 index 0000000..8e15199 --- /dev/null +++ b/packages/moxplatform_platform_interface/lib/src/platform.dart @@ -0,0 +1,7 @@ +abstract class PlatformImplementation { + /// Returns the path where persistent data should be stored. + Future getPersistentDataPath(); + + /// Returns the path where cache data should be stored. + Future getCacheDataPath(); +} diff --git a/packages/moxplatform_platform_interface/lib/src/platform_stub.dart b/packages/moxplatform_platform_interface/lib/src/platform_stub.dart new file mode 100644 index 0000000..ad85004 --- /dev/null +++ b/packages/moxplatform_platform_interface/lib/src/platform_stub.dart @@ -0,0 +1,9 @@ +import 'package:moxplatform_platform_interface/src/platform.dart'; + +class StubPlatformImplementation extends PlatformImplementation { + /// Returns the path where persistent data should be stored. + Future getPersistentDataPath() async => ""; + + /// Returns the path where cache data should be stored. + Future getCacheDataPath() async => ""; +} diff --git a/pigeons/notifications.dart b/pigeons/notifications.dart index 1aa1c54..061e1d3 100644 --- a/pigeons/notifications.dart +++ b/pigeons/notifications.dart @@ -2,12 +2,12 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon( PigeonOptions( - dartOut: 'packages/moxplatform_platform_interface/lib/src/notifications.g.dart', + dartOut: 'packages/moxplatform_platform_interface/lib/src/api.g.dart', //kotlinOut: 'packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Notifications.g.kt', //kotlinOptions: KotlinOptions( // package: 'me.polynom.moxplatform_android', //), - javaOut: 'packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Notifications.java', + javaOut: 'packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Api.java', javaOptions: JavaOptions( package: 'me.polynom.moxplatform_android', ), @@ -70,8 +70,12 @@ class MessagingNotification { } @HostApi() -abstract class NotificationsImplementationApi { +abstract class MoxplatformApi { void createNotificationChannel(String title, String id, bool urgent); void showMessagingNotification(MessagingNotification notification); + + String getPersistentDataPath(); + + String getCacheDataPath(); } From da851a985b8c8766f413e51803f1bbf784ebaec6 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Fri, 28 Jul 2023 12:46:02 +0200 Subject: [PATCH 03/23] Cleanup --- example/lib/main.dart | 1 + .../me/polynom/moxplatform_android/Api.java | 29 +++++++++++++++++-- .../NotificationReceiver.kt | 2 +- .../moxplatform_android/Notifications.kt | 19 +++++++++++- .../lib/src/api.g.dart | 8 ++++- pigeons/{notifications.dart => api.dart} | 5 +++- 6 files changed, 58 insertions(+), 6 deletions(-) rename pigeons/{notifications.dart => api.dart} (91%) diff --git a/example/lib/main.dart b/example/lib/main.dart index 190aa62..4602aac 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -170,6 +170,7 @@ class MyHomePage extends StatelessWidget { title: 'Test conversation', messages: messages, channelId: channelId, + jid: 'testjid', ), ); }, 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 beb1e1a..c1b3eb7 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 @@ -335,6 +335,20 @@ public class Api { this.channelId = setterArg; } + /** The JID of the chat in which the notifications happen. */ + 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; + } + /** Messages to show. */ private @NonNull List messages; @@ -375,6 +389,13 @@ public class Api { return this; } + private @Nullable String jid; + + public @NonNull Builder setJid(@NonNull String setterArg) { + this.jid = setterArg; + return this; + } + private @Nullable List messages; public @NonNull Builder setMessages(@NonNull List setterArg) { @@ -387,6 +408,7 @@ public class Api { pigeonReturn.setTitle(title); pigeonReturn.setId(id); pigeonReturn.setChannelId(channelId); + pigeonReturn.setJid(jid); pigeonReturn.setMessages(messages); return pigeonReturn; } @@ -394,10 +416,11 @@ public class Api { @NonNull ArrayList toList() { - ArrayList toListResult = new ArrayList(4); + ArrayList toListResult = new ArrayList(5); toListResult.add(title); toListResult.add(id); toListResult.add(channelId); + toListResult.add(jid); toListResult.add(messages); return toListResult; } @@ -410,7 +433,9 @@ public class Api { pigeonResult.setId((id == null) ? null : ((id instanceof Integer) ? (Integer) id : (Long) id)); Object channelId = list.get(2); pigeonResult.setChannelId((String) channelId); - Object messages = list.get(3); + Object jid = list.get(3); + pigeonResult.setJid((String) jid); + Object messages = list.get(4); pigeonResult.setMessages((List) messages); return pigeonResult; } 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 fa38e2d..cd022ad 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 @@ -14,7 +14,7 @@ class NotificationReceiver : BroadcastReceiver() { // send a notification to the app. // TODO: Notify app if (intent.action == MARK_AS_READ_ACTION) { - Log.d("NotificationReceiver", "Marking ${intent.getStringExtra("title")} as read") + Log.d("NotificationReceiver", "Marking ${intent.getStringExtra("jid")} as read") NotificationManagerCompat.from(context).cancel(intent.getLongExtra(MARK_AS_READ_ID_KEY, -1).toInt()) return } 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 907e233..140b629 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 @@ -41,7 +41,7 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif val markAsReadIntent = Intent(context, NotificationReceiver::class.java).apply { action = MARK_AS_READ_ACTION // TODO: Put the JID here - putExtra("title", notification.title) + putExtra("jid", notification.jid) } val markAsReadPendingIntent = PendingIntent.getBroadcast( context.applicationContext, @@ -50,6 +50,19 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif 0, ) + // -> 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 { + putExtra("jid", notification.jid) + } + val tapPendingIntent = PendingIntent.getActivity( + context, + notification.id.toInt(), + tapIntent, + PendingIntent.FLAG_UPDATE_CURRENT + ) + // Build the notification // TODO: Use a person // TODO: i18n @@ -97,6 +110,10 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif // TODO: I think this is wrong setSmallIcon(R.drawable.ic_service_icon) + // Tap action + setContentIntent(tapPendingIntent) + + // Notification actions addAction(replyAction) addAction( // TODO: Wrong icon diff --git a/packages/moxplatform_platform_interface/lib/src/api.g.dart b/packages/moxplatform_platform_interface/lib/src/api.g.dart index 09299ef..089454a 100644 --- a/packages/moxplatform_platform_interface/lib/src/api.g.dart +++ b/packages/moxplatform_platform_interface/lib/src/api.g.dart @@ -92,6 +92,7 @@ class MessagingNotification { required this.title, required this.id, required this.channelId, + required this.jid, required this.messages, }); @@ -104,6 +105,9 @@ class MessagingNotification { /// The id of the notification channel the notification should appear on. String channelId; + /// The JID of the chat in which the notifications happen. + String jid; + /// Messages to show. List messages; @@ -112,6 +116,7 @@ class MessagingNotification { title, id, channelId, + jid, messages, ]; } @@ -122,7 +127,8 @@ class MessagingNotification { title: result[0]! as String, id: result[1]! as int, channelId: result[2]! as String, - messages: (result[3] as List?)!.cast(), + jid: result[3]! as String, + messages: (result[4] as List?)!.cast(), ); } } diff --git a/pigeons/notifications.dart b/pigeons/api.dart similarity index 91% rename from pigeons/notifications.dart rename to pigeons/api.dart index 061e1d3..14d8a61 100644 --- a/pigeons/notifications.dart +++ b/pigeons/api.dart @@ -54,7 +54,7 @@ class NotificationMessage { } class MessagingNotification { - const MessagingNotification(this.title, this.id, this.messages, this.channelId); + const MessagingNotification(this.title, this.id, this.jid, this.messages, this.channelId); /// The title of the conversation. final String title; @@ -65,6 +65,9 @@ class MessagingNotification { /// The id of the notification channel the notification should appear on. final String channelId; + /// The JID of the chat in which the notifications happen. + final String jid; + /// Messages to show. final List messages; } From fb9dab3d1e79d09becd2e204a9dd9a4e5adc330b Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Fri, 28 Jul 2023 13:54:57 +0200 Subject: [PATCH 04/23] 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); } From f90b3866abd3d6c12cb7e0dd878ac5060606cbe9 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Fri, 28 Jul 2023 14:11:13 +0200 Subject: [PATCH 05/23] Handle tapping the notification --- .../polynom/moxplatform_android/Constants.kt | 2 + .../NotificationReceiver.kt | 44 ++++++++++++++----- .../moxplatform_android/Notifications.kt | 10 +++-- 3 files changed, 42 insertions(+), 14 deletions(-) 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 4c70206..31b9696 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 @@ -15,6 +15,8 @@ const val MARK_AS_READ_ACTION = "mark_as_read" // The key for the notification id to mark as read const val MARK_AS_READ_ID_KEY = "notification_id" +const val TAP_ACTION = "tap"; + // TODO: Maybe try again to rewrite the entire plugin in Kotlin //const val METHOD_CHANNEL_KEY = "me.polynom.moxplatform_android" //const val BACKGROUND_METHOD_CHANNEL_KEY = METHOD_CHANNEL_KEY + "_bg" 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 a925263..5a9946e 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 @@ -1,6 +1,7 @@ package me.polynom.moxplatform_android import android.app.NotificationManager +import android.app.PendingIntent import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -10,6 +11,18 @@ import androidx.core.app.RemoteInput import me.polynom.moxplatform_android.Api.NotificationEvent class NotificationReceiver : BroadcastReceiver() { + private fun dismissNotification(context: Context, intent: Intent) { + // 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 handleMarkAsRead(context: Context, intent: Intent) { Log.d("NotificationReceiver", "Marking ${intent.getStringExtra("jid")} as read") val jidWrapper = intent.getStringExtra("jid") ?: "" @@ -24,15 +37,7 @@ class NotificationReceiver : BroadcastReceiver() { }.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") - } + dismissNotification(context, intent); } private fun handleReply(context: Context, intent: Intent) { @@ -52,6 +57,25 @@ class NotificationReceiver : BroadcastReceiver() { // TODO: Update the notification to prevent showing the spinner } + private fun handleTap(context: Context, intent: Intent) { + Log.d("NotificationReceiver", "Received a tap") + + MoxplatformAndroidPlugin.notificationSink?.success( + NotificationEvent().apply { + jid = intent.getStringExtra("jid")!! + type = Api.NotificationEventType.OPEN + payload = null + }.toList() + ) + + // Bring the app into the foreground + val tapIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)!! + context.startActivity(tapIntent) + + // Dismiss the notification + dismissNotification(context, intent) + } + 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 @@ -59,7 +83,7 @@ class NotificationReceiver : BroadcastReceiver() { when (intent.action) { MARK_AS_READ_ACTION -> handleMarkAsRead(context, intent) REPLY_ACTION -> handleReply(context, intent) - // TODO: Handle tap + TAP_ACTION -> handleTap(context, intent) } } } \ 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 034d53e..899c78e 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 @@ -39,6 +39,7 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif replyPendingIntent, ).apply { addRemoteInput(remoteInput) + setAllowGeneratedReplies(true) }.build() // -> Mark as read action @@ -64,12 +65,13 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif // -> 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 + val tapIntent = Intent(context, NotificationReceiver::class.java).apply { + action = TAP_ACTION + // TODO: Use constants putExtra("jid", notification.jid) + putExtra("notification_id", notification.id) } - val tapPendingIntent = PendingIntent.getActivity( + val tapPendingIntent = PendingIntent.getBroadcast( context, notification.id.toInt(), tapIntent, From adb8ee88d1ff09b0e4c7e9338c52748119c21dd0 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Fri, 28 Jul 2023 17:32:14 +0200 Subject: [PATCH 06/23] feat: Take care of i18n --- example/lib/main.dart | 11 +- .../me/polynom/moxplatform_android/Api.java | 116 +++++++++++++++++- .../moxplatform_android/BootReceiver.java | 10 +- .../polynom/moxplatform_android/Constants.kt | 10 +- .../MoxplatformAndroidPlugin.java | 7 +- .../NotificationReceiver.kt | 70 +++++++++-- .../moxplatform_android/Notifications.kt | 34 ++--- .../lib/src/notifications_android.dart | 3 +- .../lib/src/api.g.dart | 49 +++++++- .../lib/src/notifications.dart | 2 +- .../lib/src/notifications_stub.dart | 2 +- pigeons/api.dart | 15 ++- 12 files changed, 277 insertions(+), 52 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index ada36f7..0ad4575 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -41,7 +41,16 @@ class MyAppState extends State { Future initStateAsync() async { await Permission.notification.request(); - await MoxplatformPlugin.notifications.createNotificationChannel("Test notification channel", channelId, false); + await MoxplatformPlugin.notifications.createNotificationChannel( + "Test notification channel", + channelId, + false, + NotificationI18nData( + reply: "答える", + markAsRead: "読みた", + you: "あなた", + ), + ); MoxplatformPlugin.notifications.getEventStream().listen((event) { print('NotificationEvent(type: ${event.type}, jid: ${event.jid}, payload: ${event.payload})'); 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 f500f94..db3ce05 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 @@ -554,6 +554,106 @@ public class Api { } } + /** Generated class from Pigeon that represents data sent in messages. */ + public static final class NotificationI18nData { + /** The content of the reply button. */ + private @NonNull String reply; + + public @NonNull String getReply() { + return reply; + } + + public void setReply(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"reply\" is null."); + } + this.reply = setterArg; + } + + /** The content of the "mark as read" button. */ + private @NonNull String markAsRead; + + public @NonNull String getMarkAsRead() { + return markAsRead; + } + + public void setMarkAsRead(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"markAsRead\" is null."); + } + this.markAsRead = setterArg; + } + + /** The text to show when *you* reply. */ + private @NonNull String you; + + public @NonNull String getYou() { + return you; + } + + public void setYou(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"you\" is null."); + } + this.you = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + NotificationI18nData() {} + + public static final class Builder { + + private @Nullable String reply; + + public @NonNull Builder setReply(@NonNull String setterArg) { + this.reply = setterArg; + return this; + } + + private @Nullable String markAsRead; + + public @NonNull Builder setMarkAsRead(@NonNull String setterArg) { + this.markAsRead = setterArg; + return this; + } + + private @Nullable String you; + + public @NonNull Builder setYou(@NonNull String setterArg) { + this.you = setterArg; + return this; + } + + public @NonNull NotificationI18nData build() { + NotificationI18nData pigeonReturn = new NotificationI18nData(); + pigeonReturn.setReply(reply); + pigeonReturn.setMarkAsRead(markAsRead); + pigeonReturn.setYou(you); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(3); + toListResult.add(reply); + toListResult.add(markAsRead); + toListResult.add(you); + return toListResult; + } + + static @NonNull NotificationI18nData fromList(@NonNull ArrayList list) { + NotificationI18nData pigeonResult = new NotificationI18nData(); + Object reply = list.get(0); + pigeonResult.setReply((String) reply); + Object markAsRead = list.get(1); + pigeonResult.setMarkAsRead((String) markAsRead); + Object you = list.get(2); + pigeonResult.setYou((String) you); + return pigeonResult; + } + } + private static class MoxplatformApiCodec extends StandardMessageCodec { public static final MoxplatformApiCodec INSTANCE = new MoxplatformApiCodec(); @@ -567,8 +667,10 @@ public class Api { case (byte) 129: return NotificationEvent.fromList((ArrayList) readValue(buffer)); case (byte) 130: - return NotificationMessage.fromList((ArrayList) readValue(buffer)); + return NotificationI18nData.fromList((ArrayList) readValue(buffer)); case (byte) 131: + return NotificationMessage.fromList((ArrayList) readValue(buffer)); + case (byte) 132: return NotificationMessageContent.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); @@ -583,11 +685,14 @@ public class Api { } else if (value instanceof NotificationEvent) { stream.write(129); writeValue(stream, ((NotificationEvent) value).toList()); - } else if (value instanceof NotificationMessage) { + } else if (value instanceof NotificationI18nData) { stream.write(130); + writeValue(stream, ((NotificationI18nData) value).toList()); + } else if (value instanceof NotificationMessage) { + stream.write(131); writeValue(stream, ((NotificationMessage) value).toList()); } else if (value instanceof NotificationMessageContent) { - stream.write(131); + stream.write(132); writeValue(stream, ((NotificationMessageContent) value).toList()); } else { super.writeValue(stream, value); @@ -598,7 +703,7 @@ public class Api { /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface MoxplatformApi { - void createNotificationChannel(@NonNull String title, @NonNull String id, @NonNull Boolean urgent); + void createNotificationChannel(@NonNull String title, @NonNull String id, @NonNull Boolean urgent, @NonNull NotificationI18nData i18n); void showMessagingNotification(@NonNull MessagingNotification notification); @@ -628,8 +733,9 @@ public class Api { String titleArg = (String) args.get(0); String idArg = (String) args.get(1); Boolean urgentArg = (Boolean) args.get(2); + NotificationI18nData i18nArg = (NotificationI18nData) args.get(3); try { - api.createNotificationChannel(titleArg, idArg, urgentArg); + api.createNotificationChannel(titleArg, idArg, urgentArg, i18nArg); wrapped.add(0, null); } catch (Throwable exception) { diff --git a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/BootReceiver.java b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/BootReceiver.java index 372d141..6603649 100644 --- a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/BootReceiver.java +++ b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/BootReceiver.java @@ -16,11 +16,11 @@ public class BootReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (MoxplatformAndroidPlugin.getStartAtBoot(context)) { - if (BackgroundService.wakeLock == null) { - Log.d(TAG, "Wakelock is null. Acquiring it..."); - BackgroundService.getLock(context).acquire(MoxplatformConstants.WAKE_LOCK_DURATION); - Log.d(TAG, "Wakelock acquired..."); - } + if (BackgroundService.wakeLock == null) { + Log.d(TAG, "Wakelock is null. Acquiring it..."); + BackgroundService.getLock(context).acquire(MoxplatformConstants.WAKE_LOCK_DURATION); + Log.d(TAG, "Wakelock acquired..."); + } ContextCompat.startForegroundService(context, new Intent(context, BackgroundService.class)); } 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 31b9696..78b8798 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,17 +6,21 @@ 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" -// The action for pressing the "Mark as read" button on a notification -const val MARK_AS_READ_ACTION = "mark_as_read" // The key for the notification id to mark as read const val MARK_AS_READ_ID_KEY = "notification_id" +// Values for actions performed through the notification +const val REPLY_ACTION = "reply"; +const val MARK_AS_READ_ACTION = "mark_as_read" const val TAP_ACTION = "tap"; +// Extra data keys for the intents that reach the NotificationReceiver +const val NOTIFICATION_EXTRA_JID_KEY = "jid"; +const val NOTIFICATION_EXTRA_ID_KEY = "notification_id"; + // TODO: Maybe try again to rewrite the entire plugin in Kotlin //const val METHOD_CHANNEL_KEY = "me.polynom.moxplatform_android" //const val BACKGROUND_METHOD_CHANNEL_KEY = METHOD_CHANNEL_KEY + "_bg" 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 e9d06ea..55d5342 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 @@ -288,7 +288,7 @@ import kotlin.jvm.functions.Function1; } @Override - public void createNotificationChannel(@NonNull String title, @NonNull String id, @NonNull Boolean urgent) { + public void createNotificationChannel(@NonNull String title, @NonNull String id, @NonNull Boolean urgent, @NonNull NotificationI18nData i18n) { final NotificationChannel channel = new NotificationChannel( id, title, @@ -298,6 +298,11 @@ import kotlin.jvm.functions.Function1; channel.enableLights(true); final NotificationManager manager = getSystemService(context, NotificationManager.class); manager.createNotificationChannel(channel); + + // Configure i18n + NotificationI18nManager.INSTANCE.setYou(i18n.getYou()); + NotificationI18nManager.INSTANCE.setReply(i18n.getReply()); + NotificationI18nManager.INSTANCE.setMarkAsRead(i18n.getMarkAsRead()); } @Override 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 5a9946e..7e7c03b 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 @@ -1,19 +1,27 @@ package me.polynom.moxplatform_android +import android.app.Notification import android.app.NotificationManager import android.app.PendingIntent import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.os.Build import android.util.Log +import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import androidx.core.app.Person import androidx.core.app.RemoteInput import me.polynom.moxplatform_android.Api.NotificationEvent +import java.time.Instant class NotificationReceiver : BroadcastReceiver() { + /* + * Dismisses the notification through which we received @intent. + * */ private fun dismissNotification(context: Context, intent: Intent) { // Dismiss the notification - val notificationId = intent.getLongExtra("notification_id", -1).toInt() + val notificationId = intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1).toInt() if (notificationId != -1) { NotificationManagerCompat.from(context).cancel( notificationId, @@ -23,15 +31,19 @@ class NotificationReceiver : BroadcastReceiver() { } } + private fun findActiveNotification(context: Context, id: Int): Notification? { + return (context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager) + .activeNotifications + .find { it.id == id }?.notification + } + 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 + jid = intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!! type = Api.NotificationEventType.MARK_AS_READ payload = null }.toList() @@ -41,28 +53,60 @@ class NotificationReceiver : BroadcastReceiver() { } private fun handleReply(context: Context, intent: Intent) { - val jidWrapper = intent.getStringExtra("jid") ?: "" val remoteInput = RemoteInput.getResultsFromIntent(intent) ?: return - Log.d("NotificationReceiver", "Got a reply for ${jidWrapper}") - // TODO: Notify app + val replyPayload = remoteInput.getCharSequence(REPLY_TEXT_KEY) MoxplatformAndroidPlugin.notificationSink?.success( NotificationEvent().apply { // TODO: Use constant for key - jid = jidWrapper + jid = intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!! type = Api.NotificationEventType.REPLY - payload = remoteInput.getCharSequence(REPLY_TEXT_KEY).toString() + payload = replyPayload.toString() }.toList() ) - // TODO: Update the notification to prevent showing the spinner + val id = intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1).toInt() + if (id == -1) { + Log.e(TAG, "Failed to find notification id for reply") + return; + } + + val notification = findActiveNotification(context, id) + if (notification == null) { + Log.e(TAG, "Failed to find notification for id ${id}") + return + } + + // Thanks https://medium.com/@sidorovroman3/android-how-to-use-messagingstyle-for-notifications-without-caching-messages-c414ef2b816c + val recoveredStyle = NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(notification)!! + // TODO: Use a person and cache this data somewhere + val newStyle = Notification.MessagingStyle(NotificationI18nManager.you).apply { + conversationTitle = recoveredStyle.conversationTitle + // TODO: Use person + recoveredStyle.messages.forEach { + addMessage(Notification.MessagingStyle.Message(it.text, it.timestamp, it.sender)) + } + } + + // TODO: Images get lost here? Do we have to request a new content URI? + newStyle.addMessage( + Notification.MessagingStyle.Message( + replyPayload!!, + Instant.now().toEpochMilli(), + null as CharSequence? + ) + ) + + val recoveredBuilder = Notification.Builder.recoverBuilder(context, notification).apply { + style = newStyle + setOnlyAlertOnce(true) + } + NotificationManagerCompat.from(context).notify(id, recoveredBuilder.build()) } private fun handleTap(context: Context, intent: Intent) { - Log.d("NotificationReceiver", "Received a tap") - MoxplatformAndroidPlugin.notificationSink?.success( NotificationEvent().apply { - jid = intent.getStringExtra("jid")!! + jid = intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!! type = Api.NotificationEventType.OPEN payload = null }.toList() 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 899c78e..ecaa52a 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 @@ -12,18 +12,23 @@ import androidx.core.content.FileProvider import androidx.core.graphics.drawable.IconCompat import java.io.File +object NotificationI18nManager { + var you: String = "You" + var markAsRead: String = "Mark as read" + var reply: String = "Reply" +} + /// Show a messaging style notification described by @notification. fun showMessagingNotification(context: Context, notification: Api.MessagingNotification) { // Build the actions // -> Reply action val remoteInput = RemoteInput.Builder(REPLY_TEXT_KEY).apply { - // TODO: i18n - setLabel("Reply") + setLabel(NotificationI18nManager.reply) }.build() val replyIntent = Intent(context, NotificationReceiver::class.java).apply { action = REPLY_ACTION - // TODO: Use a constant - putExtra("jid", notification.jid) + putExtra(NOTIFICATION_EXTRA_JID_KEY, notification.jid) + putExtra(NOTIFICATION_EXTRA_ID_KEY, notification.id) } val replyPendingIntent = PendingIntent.getBroadcast( context.applicationContext, @@ -34,8 +39,7 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif val replyAction = NotificationCompat.Action.Builder( // TODO: Wrong icon? R.drawable.ic_service_icon, - // TODO: i18n - "Reply", + NotificationI18nManager.reply, replyPendingIntent, ).apply { addRemoteInput(remoteInput) @@ -45,9 +49,8 @@ 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: Use a constant - putExtra("jid", notification.jid) - putExtra("notification_id", notification.id) + putExtra(NOTIFICATION_EXTRA_JID_KEY, notification.jid) + putExtra(NOTIFICATION_EXTRA_ID_KEY, notification.id) } val markAsReadPendingIntent = PendingIntent.getBroadcast( context.applicationContext, @@ -59,7 +62,7 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif // TODO: Wrong icon R.drawable.ic_service_icon, // TODO: i18n - "Mark as read", + NotificationI18nManager.markAsRead, markAsReadPendingIntent, ).build() @@ -67,9 +70,8 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif // Thanks https://github.com/MaikuB/flutter_local_notifications/blob/master/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java#L246 val tapIntent = Intent(context, NotificationReceiver::class.java).apply { action = TAP_ACTION - // TODO: Use constants - putExtra("jid", notification.jid) - putExtra("notification_id", notification.id) + putExtra(NOTIFICATION_EXTRA_JID_KEY, notification.jid) + putExtra(NOTIFICATION_EXTRA_ID_KEY, notification.id) } val tapPendingIntent = PendingIntent.getBroadcast( context, @@ -80,8 +82,7 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif // Build the notification // TODO: Use a person - // TODO: i18n - val style = NotificationCompat.MessagingStyle("Me"); + val style = NotificationCompat.MessagingStyle(NotificationI18nManager.you); for (message in notification.messages) { // Build the sender val sender = Person.Builder().apply { @@ -131,6 +132,9 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif // Notification actions addAction(replyAction) addAction(markAsReadAction) + + // Prevent no notification when we replied before + setOnlyAlertOnce(false) }.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 2a2243e..ff14296 100644 --- a/packages/moxplatform_android/lib/src/notifications_android.dart +++ b/packages/moxplatform_android/lib/src/notifications_android.dart @@ -13,8 +13,9 @@ class AndroidNotificationsImplementation extends NotificationsImplementation { String title, String id, bool urgent, + NotificationI18nData i18n, ) async { - return _api.createNotificationChannel(title, id, urgent); + return _api.createNotificationChannel(title, id, urgent, i18n); } @override diff --git a/packages/moxplatform_platform_interface/lib/src/api.g.dart b/packages/moxplatform_platform_interface/lib/src/api.g.dart index 96ea483..42f8a0f 100644 --- a/packages/moxplatform_platform_interface/lib/src/api.g.dart +++ b/packages/moxplatform_platform_interface/lib/src/api.g.dart @@ -175,6 +175,40 @@ class NotificationEvent { } } +class NotificationI18nData { + NotificationI18nData({ + required this.reply, + required this.markAsRead, + required this.you, + }); + + /// The content of the reply button. + String reply; + + /// The content of the "mark as read" button. + String markAsRead; + + /// The text to show when *you* reply. + String you; + + Object encode() { + return [ + reply, + markAsRead, + you, + ]; + } + + static NotificationI18nData decode(Object result) { + result as List; + return NotificationI18nData( + reply: result[0]! as String, + markAsRead: result[1]! as String, + you: result[2]! as String, + ); + } +} + class _MoxplatformApiCodec extends StandardMessageCodec { const _MoxplatformApiCodec(); @override @@ -185,12 +219,15 @@ class _MoxplatformApiCodec extends StandardMessageCodec { } else if (value is NotificationEvent) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is NotificationMessage) { + } else if (value is NotificationI18nData) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is NotificationMessageContent) { + } else if (value is NotificationMessage) { buffer.putUint8(131); writeValue(buffer, value.encode()); + } else if (value is NotificationMessageContent) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -204,8 +241,10 @@ class _MoxplatformApiCodec extends StandardMessageCodec { case 129: return NotificationEvent.decode(readValue(buffer)!); case 130: - return NotificationMessage.decode(readValue(buffer)!); + return NotificationI18nData.decode(readValue(buffer)!); case 131: + return NotificationMessage.decode(readValue(buffer)!); + case 132: return NotificationMessageContent.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -223,12 +262,12 @@ class MoxplatformApi { static const MessageCodec codec = _MoxplatformApiCodec(); - Future createNotificationChannel(String arg_title, String arg_id, bool arg_urgent) async { + Future createNotificationChannel(String arg_title, String arg_id, bool arg_urgent, NotificationI18nData arg_i18n) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.createNotificationChannel', codec, binaryMessenger: _binaryMessenger); final List? replyList = - await channel.send([arg_title, arg_id, arg_urgent]) as List?; + await channel.send([arg_title, arg_id, arg_urgent, arg_i18n]) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', diff --git a/packages/moxplatform_platform_interface/lib/src/notifications.dart b/packages/moxplatform_platform_interface/lib/src/notifications.dart index 8b5ff48..bcf9fc5 100644 --- a/packages/moxplatform_platform_interface/lib/src/notifications.dart +++ b/packages/moxplatform_platform_interface/lib/src/notifications.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:moxplatform_platform_interface/src/api.g.dart'; abstract class NotificationsImplementation { - Future createNotificationChannel(String title, String id, bool urgent); + Future createNotificationChannel(String title, String id, bool urgent, NotificationI18nData i18n); Future showMessagingNotification(MessagingNotification notification); diff --git a/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart b/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart index c6dd341..40e2f71 100644 --- a/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart +++ b/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart @@ -4,7 +4,7 @@ import 'package:moxplatform_platform_interface/src/notifications.dart'; class StubNotificationsImplementation extends NotificationsImplementation { @override - Future createNotificationChannel(String title, String id, bool urgent) async {} + Future createNotificationChannel(String title, String id, bool urgent, NotificationI18nData i18n) async {} @override Future showMessagingNotification(MessagingNotification notification) async {} diff --git a/pigeons/api.dart b/pigeons/api.dart index 7634510..d8471cd 100644 --- a/pigeons/api.dart +++ b/pigeons/api.dart @@ -97,9 +97,22 @@ class NotificationEvent { final String? payload; } +class NotificationI18nData { + const NotificationI18nData(this.reply, this.markAsRead, this.you); + + /// The content of the reply button. + final String reply; + + /// The content of the "mark as read" button. + final String markAsRead; + + /// The text to show when *you* reply. + final String you; +} + @HostApi() abstract class MoxplatformApi { - void createNotificationChannel(String title, String id, bool urgent); + void createNotificationChannel(String title, String id, bool urgent, NotificationI18nData i18n); void showMessagingNotification(MessagingNotification notification); From e975e749e45e4c1638462fa3a32562ae5dc5ad83 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Fri, 28 Jul 2023 21:06:50 +0200 Subject: [PATCH 07/23] fix: Fix images disappearing after replying --- .../polynom/moxplatform_android/Constants.kt | 14 +++++--- .../NotificationReceiver.kt | 33 +++++++++++++++++-- .../moxplatform_android/Notifications.kt | 11 +++++-- 3 files changed, 50 insertions(+), 8 deletions(-) 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 78b8798..c23ba9f 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 @@ -13,13 +13,19 @@ const val REPLY_TEXT_KEY = "key_reply_text" const val MARK_AS_READ_ID_KEY = "notification_id" // Values for actions performed through the notification -const val REPLY_ACTION = "reply"; +const val REPLY_ACTION = "reply" const val MARK_AS_READ_ACTION = "mark_as_read" -const val TAP_ACTION = "tap"; +const val TAP_ACTION = "tap" // Extra data keys for the intents that reach the NotificationReceiver -const val NOTIFICATION_EXTRA_JID_KEY = "jid"; -const val NOTIFICATION_EXTRA_ID_KEY = "notification_id"; +const val NOTIFICATION_EXTRA_JID_KEY = "jid" +const val NOTIFICATION_EXTRA_ID_KEY = "notification_id" + +// Extra data keys for messages embedded inside the notification style +const val NOTIFICATION_MESSAGE_EXTRA_MIME = "mime" +const val NOTIFICATION_MESSAGE_EXTRA_PATH = "path" + +const val MOXPLATFORM_FILEPROVIDER_ID = "me.polynom.moxplatform_android.fileprovider" // TODO: Maybe try again to rewrite the entire plugin in Kotlin //const val METHOD_CHANNEL_KEY = "me.polynom.moxplatform_android" 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 7e7c03b..6ec8bf2 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 @@ -12,7 +12,9 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.Person import androidx.core.app.RemoteInput +import androidx.core.content.FileProvider import me.polynom.moxplatform_android.Api.NotificationEvent +import java.io.File import java.time.Instant class NotificationReceiver : BroadcastReceiver() { @@ -83,11 +85,37 @@ class NotificationReceiver : BroadcastReceiver() { conversationTitle = recoveredStyle.conversationTitle // TODO: Use person recoveredStyle.messages.forEach { - addMessage(Notification.MessagingStyle.Message(it.text, it.timestamp, it.sender)) + // Check if we have to request (or refresh) the content URI to be able to still + // see the embedded image. + val mime = it.extras.getString(NOTIFICATION_MESSAGE_EXTRA_MIME) + val path = it.extras.getString(NOTIFICATION_MESSAGE_EXTRA_PATH) + val message = Notification.MessagingStyle.Message(it.text, it.timestamp, it.sender) + if (mime != null && path != null) { + // Request a new URI from the file provider to ensure we can still see the image + // in the notification + val fileUri = FileProvider.getUriForFile( + context, + MOXPLATFORM_FILEPROVIDER_ID, + File(path), + ) + message.setData( + mime, + fileUri, + ) + + // As we're creating a new message, also recreate the additional metadata + message.extras.apply { + putString(NOTIFICATION_MESSAGE_EXTRA_MIME, mime) + putString(NOTIFICATION_MESSAGE_EXTRA_PATH, path) + } + } + + // Append the old message + addMessage(message) } } - // TODO: Images get lost here? Do we have to request a new content URI? + // Append our new message newStyle.addMessage( Notification.MessagingStyle.Message( replyPayload!!, @@ -96,6 +124,7 @@ class NotificationReceiver : BroadcastReceiver() { ) ) + // Post the new notification val recoveredBuilder = Notification.Builder.recoverBuilder(context, notification).apply { style = newStyle setOnlyAlertOnce(true) 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 ecaa52a..13a2ecb 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 @@ -110,10 +110,17 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif if (message.content.mime != null && message.content.path != null) { val fileUri = FileProvider.getUriForFile( context, - "me.polynom.moxplatform_android.fileprovider", + MOXPLATFORM_FILEPROVIDER_ID, File(message.content.path), ) - msg.setData(message.content.mime, fileUri) + msg.apply { + setData(message.content.mime, fileUri) + + extras.apply { + putString(NOTIFICATION_MESSAGE_EXTRA_MIME, message.content.mime) + putString(NOTIFICATION_MESSAGE_EXTRA_PATH, message.content.path) + } + } } // Append the message From daf40aed0beb8e9136f0e86bda5e29b7a155eaa0 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Fri, 28 Jul 2023 21:46:47 +0200 Subject: [PATCH 08/23] feat: Allow setting the self-avatar --- example/lib/main.dart | 11 ++++++++ .../me/polynom/moxplatform_android/Api.java | 26 ++++++++++++++++++ .../MoxplatformAndroidPlugin.java | 13 ++++++--- .../NotificationReceiver.kt | 27 ++++++++++++++----- .../moxplatform_android/Notifications.kt | 25 ++++++++++++----- .../lib/src/notifications_android.dart | 5 ++++ .../lib/src/api.g.dart | 22 +++++++++++++++ .../lib/src/notifications.dart | 2 ++ .../lib/src/notifications_stub.dart | 3 +++ pigeons/api.dart | 2 ++ 10 files changed, 119 insertions(+), 17 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 0ad4575..3e01ac7 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -189,6 +189,17 @@ class MyHomePage extends StatelessWidget { }, child: const Text('Show messaging notification'), ), + ElevatedButton( + onPressed: () async { + final result = await FilePicker.platform.pickFiles( + type: FileType.image, + ); + if (result == null) return; + + MoxplatformPlugin.notifications.setNotificationSelfAvatar(result.files.single.path!); + }, + child: const Text('Set notification self-avatar'), + ), ElevatedButton( onPressed: () async { print(await MoxplatformPlugin.platform.getPersistentDataPath()); 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 db3ce05..c53cdf9 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 @@ -707,6 +707,8 @@ public class Api { void showMessagingNotification(@NonNull MessagingNotification notification); + void setNotificationSelfAvatar(@NonNull String path); + @NonNull String getPersistentDataPath(); @@ -762,6 +764,30 @@ public class Api { api.showMessagingNotification(notificationArg); wrapped.add(0, null); } + 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.setNotificationSelfAvatar", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + String pathArg = (String) args.get(0); + try { + api.setNotificationSelfAvatar(pathArg); + 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/MoxplatformAndroidPlugin.java b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/MoxplatformAndroidPlugin.java index 55d5342..c85f8b6 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 @@ -300,9 +300,9 @@ import kotlin.jvm.functions.Function1; manager.createNotificationChannel(channel); // Configure i18n - NotificationI18nManager.INSTANCE.setYou(i18n.getYou()); - NotificationI18nManager.INSTANCE.setReply(i18n.getReply()); - NotificationI18nManager.INSTANCE.setMarkAsRead(i18n.getMarkAsRead()); + NotificationDataManager.INSTANCE.setYou(i18n.getYou()); + NotificationDataManager.INSTANCE.setReply(i18n.getReply()); + NotificationDataManager.INSTANCE.setMarkAsRead(i18n.getMarkAsRead()); } @Override @@ -310,7 +310,12 @@ import kotlin.jvm.functions.Function1; NotificationsKt.showMessagingNotification(context, notification); } - @NonNull + @Override + public void setNotificationSelfAvatar(@NonNull String path) { + NotificationDataManager.INSTANCE.setAvatarPath(path); + } + + @NonNull @Override public String getPersistentDataPath() { return context.getFilesDir().getPath(); 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 6ec8bf2..800f2e8 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 @@ -6,6 +6,8 @@ import android.app.PendingIntent import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.graphics.BitmapFactory +import android.graphics.drawable.Icon import android.os.Build import android.util.Log import androidx.core.app.NotificationCompat @@ -13,6 +15,7 @@ import androidx.core.app.NotificationManagerCompat import androidx.core.app.Person import androidx.core.app.RemoteInput import androidx.core.content.FileProvider +import androidx.core.graphics.drawable.IconCompat import me.polynom.moxplatform_android.Api.NotificationEvent import java.io.File import java.time.Instant @@ -43,8 +46,6 @@ class NotificationReceiver : BroadcastReceiver() { 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 = intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!! type = Api.NotificationEventType.MARK_AS_READ payload = null @@ -59,7 +60,6 @@ class NotificationReceiver : BroadcastReceiver() { val replyPayload = remoteInput.getCharSequence(REPLY_TEXT_KEY) MoxplatformAndroidPlugin.notificationSink?.success( NotificationEvent().apply { - // TODO: Use constant for key jid = intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!! type = Api.NotificationEventType.REPLY payload = replyPayload.toString() @@ -80,10 +80,25 @@ class NotificationReceiver : BroadcastReceiver() { // Thanks https://medium.com/@sidorovroman3/android-how-to-use-messagingstyle-for-notifications-without-caching-messages-c414ef2b816c val recoveredStyle = NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(notification)!! - // TODO: Use a person and cache this data somewhere - val newStyle = Notification.MessagingStyle(NotificationI18nManager.you).apply { + val newStyle = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + Notification.MessagingStyle( + android.app.Person.Builder().apply { + setName(NotificationDataManager.you) + + // Set an avatar, if we have one + if (NotificationDataManager.avatarPath != null) { + setIcon( + Icon.createWithAdaptiveBitmap( + BitmapFactory.decodeFile(NotificationDataManager.avatarPath) + ) + ) + } + }.build() + ) + else Notification.MessagingStyle(NotificationDataManager.you) + + newStyle.apply { conversationTitle = recoveredStyle.conversationTitle - // TODO: Use person recoveredStyle.messages.forEach { // Check if we have to request (or refresh) the content URI to be able to still // see the embedded image. 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 13a2ecb..81c7b0f 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 @@ -12,10 +12,11 @@ import androidx.core.content.FileProvider import androidx.core.graphics.drawable.IconCompat import java.io.File -object NotificationI18nManager { +object NotificationDataManager { var you: String = "You" var markAsRead: String = "Mark as read" var reply: String = "Reply" + var avatarPath: String? = null } /// Show a messaging style notification described by @notification. @@ -23,7 +24,7 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif // Build the actions // -> Reply action val remoteInput = RemoteInput.Builder(REPLY_TEXT_KEY).apply { - setLabel(NotificationI18nManager.reply) + setLabel(NotificationDataManager.reply) }.build() val replyIntent = Intent(context, NotificationReceiver::class.java).apply { action = REPLY_ACTION @@ -39,7 +40,7 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif val replyAction = NotificationCompat.Action.Builder( // TODO: Wrong icon? R.drawable.ic_service_icon, - NotificationI18nManager.reply, + NotificationDataManager.reply, replyPendingIntent, ).apply { addRemoteInput(remoteInput) @@ -61,8 +62,7 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif val markAsReadAction = NotificationCompat.Action.Builder( // TODO: Wrong icon R.drawable.ic_service_icon, - // TODO: i18n - NotificationI18nManager.markAsRead, + NotificationDataManager.markAsRead, markAsReadPendingIntent, ).build() @@ -81,8 +81,19 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif ) // Build the notification - // TODO: Use a person - val style = NotificationCompat.MessagingStyle(NotificationI18nManager.you); + val selfPerson = Person.Builder().apply { + setName(NotificationDataManager.you) + + // Set an avatar, if we have one + if (NotificationDataManager.avatarPath != null) { + setIcon( + IconCompat.createWithAdaptiveBitmap( + BitmapFactory.decodeFile(NotificationDataManager.avatarPath), + ), + ) + } + }.build() + val style = NotificationCompat.MessagingStyle(selfPerson); for (message in notification.messages) { // Build the sender val sender = Person.Builder().apply { diff --git a/packages/moxplatform_android/lib/src/notifications_android.dart b/packages/moxplatform_android/lib/src/notifications_android.dart index ff14296..45cb9b6 100644 --- a/packages/moxplatform_android/lib/src/notifications_android.dart +++ b/packages/moxplatform_android/lib/src/notifications_android.dart @@ -25,6 +25,11 @@ class AndroidNotificationsImplementation extends NotificationsImplementation { return _api.showMessagingNotification(notification); } + @override + Future setNotificationSelfAvatar(String path) async { + return _api.setNotificationSelfAvatar(path); + } + @override Stream getEventStream() => _channel .receiveBroadcastStream() diff --git a/packages/moxplatform_platform_interface/lib/src/api.g.dart b/packages/moxplatform_platform_interface/lib/src/api.g.dart index 42f8a0f..893266b 100644 --- a/packages/moxplatform_platform_interface/lib/src/api.g.dart +++ b/packages/moxplatform_platform_interface/lib/src/api.g.dart @@ -306,6 +306,28 @@ class MoxplatformApi { } } + Future setNotificationSelfAvatar(String arg_path) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.setNotificationSelfAvatar', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_path]) 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 getPersistentDataPath() async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.getPersistentDataPath', codec, diff --git a/packages/moxplatform_platform_interface/lib/src/notifications.dart b/packages/moxplatform_platform_interface/lib/src/notifications.dart index bcf9fc5..abdf1a0 100644 --- a/packages/moxplatform_platform_interface/lib/src/notifications.dart +++ b/packages/moxplatform_platform_interface/lib/src/notifications.dart @@ -6,5 +6,7 @@ abstract class NotificationsImplementation { Future showMessagingNotification(MessagingNotification notification); + Future setNotificationSelfAvatar(String path); + 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 40e2f71..c1b7fcc 100644 --- a/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart +++ b/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart @@ -9,6 +9,9 @@ class StubNotificationsImplementation extends NotificationsImplementation { @override Future showMessagingNotification(MessagingNotification notification) async {} + @override + Future setNotificationSelfAvatar(String path) async {} + @override Stream getEventStream() { return StreamController().stream; diff --git a/pigeons/api.dart b/pigeons/api.dart index d8471cd..0b951ee 100644 --- a/pigeons/api.dart +++ b/pigeons/api.dart @@ -116,6 +116,8 @@ abstract class MoxplatformApi { void showMessagingNotification(MessagingNotification notification); + void setNotificationSelfAvatar(String path); + String getPersistentDataPath(); String getCacheDataPath(); From 8f9382161739bb08492c96b3542001a7ee34192e Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sat, 29 Jul 2023 12:34:40 +0200 Subject: [PATCH 09/23] feat: Color in the notification silhouette --- example/lib/main.dart | 7 +++-- .../me/polynom/moxplatform_android/Api.java | 31 +++++++++++++++++-- .../MoxplatformAndroidPlugin.java | 15 +++++---- .../moxplatform_android/Notifications.kt | 16 +++++++--- .../src/main/res/drawable/mark_as_read.xml | 5 +++ .../android/src/main/res/drawable/reply.xml | 5 +++ .../lib/src/notifications_android.dart | 10 ++++-- .../lib/src/api.g.dart | 26 ++++++++++++++-- .../lib/src/notifications.dart | 9 +++++- .../lib/src/notifications_stub.dart | 5 ++- pigeons/api.dart | 8 ++--- 11 files changed, 107 insertions(+), 30 deletions(-) create mode 100644 packages/moxplatform_android/android/src/main/res/drawable/mark_as_read.xml create mode 100644 packages/moxplatform_android/android/src/main/res/drawable/reply.xml diff --git a/example/lib/main.dart b/example/lib/main.dart index 3e01ac7..b4cd52a 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -45,6 +45,8 @@ class MyAppState extends State { "Test notification channel", channelId, false, + ); + await MoxplatformPlugin.notifications.setI18n( NotificationI18nData( reply: "答える", markAsRead: "読みた", @@ -128,9 +130,8 @@ class MyHomePage extends StatelessWidget { title: const Text('Moxplatform Demo'), ), body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ + child: ListView( + children: [ ElevatedButton( onPressed: _cryptoTest, child: const Text('Test cryptography'), 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 c53cdf9..4e910d7 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 @@ -703,12 +703,14 @@ public class Api { /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface MoxplatformApi { - void createNotificationChannel(@NonNull String title, @NonNull String id, @NonNull Boolean urgent, @NonNull NotificationI18nData i18n); + void createNotificationChannel(@NonNull String title, @NonNull String id, @NonNull Boolean urgent); void showMessagingNotification(@NonNull MessagingNotification notification); void setNotificationSelfAvatar(@NonNull String path); + void setNotificationI18n(@NonNull NotificationI18nData data); + @NonNull String getPersistentDataPath(); @@ -735,9 +737,8 @@ public class Api { String titleArg = (String) args.get(0); String idArg = (String) args.get(1); Boolean urgentArg = (Boolean) args.get(2); - NotificationI18nData i18nArg = (NotificationI18nData) args.get(3); try { - api.createNotificationChannel(titleArg, idArg, urgentArg, i18nArg); + api.createNotificationChannel(titleArg, idArg, urgentArg); wrapped.add(0, null); } catch (Throwable exception) { @@ -788,6 +789,30 @@ public class Api { api.setNotificationSelfAvatar(pathArg); wrapped.add(0, null); } + 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.setNotificationI18n", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + NotificationI18nData dataArg = (NotificationI18nData) args.get(0); + try { + api.setNotificationI18n(dataArg); + 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/MoxplatformAndroidPlugin.java b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/MoxplatformAndroidPlugin.java index c85f8b6..d1bf10b 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 @@ -288,7 +288,7 @@ import kotlin.jvm.functions.Function1; } @Override - public void createNotificationChannel(@NonNull String title, @NonNull String id, @NonNull Boolean urgent, @NonNull NotificationI18nData i18n) { + public void createNotificationChannel(@NonNull String title, @NonNull String id, @NonNull Boolean urgent) { final NotificationChannel channel = new NotificationChannel( id, title, @@ -298,11 +298,6 @@ import kotlin.jvm.functions.Function1; channel.enableLights(true); final NotificationManager manager = getSystemService(context, NotificationManager.class); manager.createNotificationChannel(channel); - - // Configure i18n - NotificationDataManager.INSTANCE.setYou(i18n.getYou()); - NotificationDataManager.INSTANCE.setReply(i18n.getReply()); - NotificationDataManager.INSTANCE.setMarkAsRead(i18n.getMarkAsRead()); } @Override @@ -315,6 +310,14 @@ import kotlin.jvm.functions.Function1; NotificationDataManager.INSTANCE.setAvatarPath(path); } + @Override + public void setNotificationI18n(@NonNull NotificationI18nData data) { + // Configure i18n + NotificationDataManager.INSTANCE.setYou(data.getYou()); + NotificationDataManager.INSTANCE.setReply(data.getReply()); + NotificationDataManager.INSTANCE.setMarkAsRead(data.getMarkAsRead()); + } + @NonNull @Override public String getPersistentDataPath() { 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 81c7b0f..337a333 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 @@ -1,9 +1,11 @@ package me.polynom.moxplatform_android +import android.app.Notification import android.app.PendingIntent import android.content.Context import android.content.Intent import android.graphics.BitmapFactory +import android.graphics.Color import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.Person @@ -38,8 +40,7 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif PendingIntent.FLAG_UPDATE_CURRENT, ) val replyAction = NotificationCompat.Action.Builder( - // TODO: Wrong icon? - R.drawable.ic_service_icon, + R.drawable.reply, NotificationDataManager.reply, replyPendingIntent, ).apply { @@ -60,8 +61,7 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif PendingIntent.FLAG_UPDATE_CURRENT, ) val markAsReadAction = NotificationCompat.Action.Builder( - // TODO: Wrong icon - R.drawable.ic_service_icon, + R.drawable.mark_as_read, NotificationDataManager.markAsRead, markAsReadPendingIntent, ).build() @@ -141,8 +141,11 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif // Assemble the notification val finalNotification = NotificationCompat.Builder(context, notification.channelId).apply { setStyle(style) - // TODO: I think this is wrong + // NOTE: It's okay to use the service icon here as I cannot get Android to display the + // actual logo. So we'll have to make do with the silhouette and the color purple. setSmallIcon(R.drawable.ic_service_icon) + color = Color.argb(255, 207, 74, 255) + setColorized(true) // Tap action setContentIntent(tapPendingIntent) @@ -151,6 +154,9 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif addAction(replyAction) addAction(markAsReadAction) + setAllowSystemGeneratedContextualActions(true) + setCategory(Notification.CATEGORY_MESSAGE) + // Prevent no notification when we replied before setOnlyAlertOnce(false) }.build() diff --git a/packages/moxplatform_android/android/src/main/res/drawable/mark_as_read.xml b/packages/moxplatform_android/android/src/main/res/drawable/mark_as_read.xml new file mode 100644 index 0000000..5b80636 --- /dev/null +++ b/packages/moxplatform_android/android/src/main/res/drawable/mark_as_read.xml @@ -0,0 +1,5 @@ + + + diff --git a/packages/moxplatform_android/android/src/main/res/drawable/reply.xml b/packages/moxplatform_android/android/src/main/res/drawable/reply.xml new file mode 100644 index 0000000..fb8b6df --- /dev/null +++ b/packages/moxplatform_android/android/src/main/res/drawable/reply.xml @@ -0,0 +1,5 @@ + + + diff --git a/packages/moxplatform_android/lib/src/notifications_android.dart b/packages/moxplatform_android/lib/src/notifications_android.dart index 45cb9b6..7fd95fa 100644 --- a/packages/moxplatform_android/lib/src/notifications_android.dart +++ b/packages/moxplatform_android/lib/src/notifications_android.dart @@ -13,9 +13,8 @@ class AndroidNotificationsImplementation extends NotificationsImplementation { String title, String id, bool urgent, - NotificationI18nData i18n, ) async { - return _api.createNotificationChannel(title, id, urgent, i18n); + return _api.createNotificationChannel(title, id, urgent); } @override @@ -28,7 +27,12 @@ class AndroidNotificationsImplementation extends NotificationsImplementation { @override Future setNotificationSelfAvatar(String path) async { return _api.setNotificationSelfAvatar(path); - } + } + + @override + Future setI18n(NotificationI18nData data) { + return _api.setNotificationI18n(data); + } @override Stream getEventStream() => _channel diff --git a/packages/moxplatform_platform_interface/lib/src/api.g.dart b/packages/moxplatform_platform_interface/lib/src/api.g.dart index 893266b..d81daaa 100644 --- a/packages/moxplatform_platform_interface/lib/src/api.g.dart +++ b/packages/moxplatform_platform_interface/lib/src/api.g.dart @@ -262,12 +262,12 @@ class MoxplatformApi { static const MessageCodec codec = _MoxplatformApiCodec(); - Future createNotificationChannel(String arg_title, String arg_id, bool arg_urgent, NotificationI18nData arg_i18n) async { + Future createNotificationChannel(String arg_title, String arg_id, bool arg_urgent) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.createNotificationChannel', codec, binaryMessenger: _binaryMessenger); final List? replyList = - await channel.send([arg_title, arg_id, arg_urgent, arg_i18n]) as List?; + await channel.send([arg_title, arg_id, arg_urgent]) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -328,6 +328,28 @@ class MoxplatformApi { } } + Future setNotificationI18n(NotificationI18nData arg_data) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.setNotificationI18n', 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 getPersistentDataPath() async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.getPersistentDataPath', codec, diff --git a/packages/moxplatform_platform_interface/lib/src/notifications.dart b/packages/moxplatform_platform_interface/lib/src/notifications.dart index abdf1a0..31ef838 100644 --- a/packages/moxplatform_platform_interface/lib/src/notifications.dart +++ b/packages/moxplatform_platform_interface/lib/src/notifications.dart @@ -2,11 +2,18 @@ import 'dart:async'; import 'package:moxplatform_platform_interface/src/api.g.dart'; abstract class NotificationsImplementation { - Future createNotificationChannel(String title, String id, bool urgent, NotificationI18nData i18n); + /// Creates a notification channel with the name [title] and id [id]. If [urgent] is true, then + /// it configures the channel as carrying urgent information. + Future createNotificationChannel(String title, String id, bool urgent); + /// Shows a notification [notification] in the messaging style with everyting it needs. Future showMessagingNotification(MessagingNotification notification); + /// Sets the path to the self-avatar for in-notification replies. Future setNotificationSelfAvatar(String path); + /// Configures the i18n data for usage in notifications. + Future setI18n(NotificationI18nData data); + 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 c1b7fcc..2bc36fe 100644 --- a/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart +++ b/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart @@ -4,7 +4,7 @@ import 'package:moxplatform_platform_interface/src/notifications.dart'; class StubNotificationsImplementation extends NotificationsImplementation { @override - Future createNotificationChannel(String title, String id, bool urgent, NotificationI18nData i18n) async {} + Future createNotificationChannel(String title, String id, bool urgent) async {} @override Future showMessagingNotification(MessagingNotification notification) async {} @@ -12,6 +12,9 @@ class StubNotificationsImplementation extends NotificationsImplementation { @override Future setNotificationSelfAvatar(String path) async {} + @override + Future setI18n(NotificationI18nData data) async {} + @override Stream getEventStream() { return StreamController().stream; diff --git a/pigeons/api.dart b/pigeons/api.dart index 0b951ee..d1877fc 100644 --- a/pigeons/api.dart +++ b/pigeons/api.dart @@ -112,15 +112,11 @@ class NotificationI18nData { @HostApi() abstract class MoxplatformApi { - void createNotificationChannel(String title, String id, bool urgent, NotificationI18nData i18n); - + void createNotificationChannel(String title, String id, bool urgent); void showMessagingNotification(MessagingNotification notification); - void setNotificationSelfAvatar(String path); - + void setNotificationI18n(NotificationI18nData data); String getPersistentDataPath(); - String getCacheDataPath(); - void eventStub(NotificationEvent event); } From 30ef477999d9aed28791b315b6f0bce60a1562c3 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sat, 29 Jul 2023 12:50:50 +0200 Subject: [PATCH 10/23] feat: Make i18n data a bit more persistent --- .../BackgroundService.java | 8 ++- .../polynom/moxplatform_android/Constants.kt | 6 ++ .../MoxplatformAndroidPlugin.java | 18 +++--- .../NotificationReceiver.kt | 6 +- .../moxplatform_android/Notifications.kt | 59 ++++++++++++++++--- 5 files changed, 75 insertions(+), 22 deletions(-) diff --git a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/BackgroundService.java b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/BackgroundService.java index 78e4d0f..d4a7df0 100644 --- a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/BackgroundService.java +++ b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/BackgroundService.java @@ -1,5 +1,7 @@ package me.polynom.moxplatform_android; +import static me.polynom.moxplatform_android.ConstantsKt.SHARED_PREFERENCES_KEY; + import android.app.AlarmManager; import android.app.NotificationChannel; import android.app.NotificationManager; @@ -85,10 +87,10 @@ public class BackgroundService extends Service implements MethodChannel.MethodCa } public static boolean isManuallyStopped(Context context) { - return context.getSharedPreferences(MoxplatformAndroidPlugin.sharedPrefKey, MODE_PRIVATE).getBoolean(manuallyStoppedKey, false); + return context.getSharedPreferences(SHARED_PREFERENCES_KEY, MODE_PRIVATE).getBoolean(manuallyStoppedKey, false); } public void setManuallyStopped(Context context, boolean value) { - context.getSharedPreferences(MoxplatformAndroidPlugin.sharedPrefKey, MODE_PRIVATE) + context.getSharedPreferences(SHARED_PREFERENCES_KEY, MODE_PRIVATE) .edit() .putBoolean(manuallyStoppedKey, value) .apply(); @@ -151,7 +153,7 @@ public class BackgroundService extends Service implements MethodChannel.MethodCa FlutterInjector.instance().flutterLoader().startInitialization(getApplicationContext()); } - long entrypointHandle = getSharedPreferences(MoxplatformAndroidPlugin.sharedPrefKey, MODE_PRIVATE) + long entrypointHandle = getSharedPreferences(SHARED_PREFERENCES_KEY, MODE_PRIVATE) .getLong(MoxplatformAndroidPlugin.entrypointKey, 0); FlutterInjector.instance().flutterLoader().ensureInitializationComplete(getApplicationContext(), null); FlutterCallbackInformation callback = FlutterCallbackInformation.lookupCallbackInformation(entrypointHandle); 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 c23ba9f..3900268 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 @@ -27,6 +27,12 @@ const val NOTIFICATION_MESSAGE_EXTRA_PATH = "path" const val MOXPLATFORM_FILEPROVIDER_ID = "me.polynom.moxplatform_android.fileprovider" +// Shared preferences keys +const val SHARED_PREFERENCES_KEY = "me.polynom.moxplatform_android" +const val SHARED_PREFERENCES_YOU_KEY = "you" +const val SHARED_PREFERENCES_MARK_AS_READ_KEY = "mark_as_read" +const val SHARED_PREFERENCES_REPLY_KEY = "reply" + // TODO: Maybe try again to rewrite the entire plugin in Kotlin //const val METHOD_CHANNEL_KEY = "me.polynom.moxplatform_android" //const val BACKGROUND_METHOD_CHANNEL_KEY = METHOD_CHANNEL_KEY + "_bg" 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 d1bf10b..599b9bf 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 @@ -1,6 +1,7 @@ package me.polynom.moxplatform_android; import static androidx.core.content.ContextCompat.getSystemService; +import static me.polynom.moxplatform_android.ConstantsKt.SHARED_PREFERENCES_KEY; import static me.polynom.moxplatform_android.RecordSentMessageKt.recordSentMessage; import static me.polynom.moxplatform_android.CryptoKt.*; import me.polynom.moxplatform_android.Api.*; @@ -41,7 +42,6 @@ import kotlin.jvm.functions.Function1; public static final String entrypointKey = "entrypoint_handle"; public static final String extraDataKey = "extra_data"; private static final String autoStartAtBootKey = "auto_start_at_boot"; - public static final String sharedPrefKey = "me.polynom.moxplatform_android"; private static final String TAG = "moxplatform_android"; public static final String methodChannelKey = "me.polynom.moxplatform_android"; public static final String dataReceivedMethodName = "dataReceived"; @@ -104,7 +104,7 @@ import kotlin.jvm.functions.Function1; /// 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); + SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE); prefs.edit() .putLong(entrypointKey, entrypointHandle) .putString(extraDataKey, extraData) @@ -112,21 +112,21 @@ import kotlin.jvm.functions.Function1; } public static long getHandle(Context c) { - return c.getSharedPreferences(sharedPrefKey, Context.MODE_PRIVATE).getLong(entrypointKey, 0); + return c.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).getLong(entrypointKey, 0); } public static String getExtraData(Context c) { - return c.getSharedPreferences(sharedPrefKey, Context.MODE_PRIVATE).getString(extraDataKey, ""); + return c.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).getString(extraDataKey, ""); } public static void setStartAtBoot(Context c, boolean value) { - c.getSharedPreferences(sharedPrefKey, Context.MODE_PRIVATE) + c.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) .edit() .putBoolean(autoStartAtBootKey, value) .apply(); } public static boolean getStartAtBoot(Context c) { - return c.getSharedPreferences(sharedPrefKey, Context.MODE_PRIVATE).getBoolean(autoStartAtBootKey, false); + return c.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).getBoolean(autoStartAtBootKey, false); } private boolean isRunning() { @@ -313,9 +313,9 @@ import kotlin.jvm.functions.Function1; @Override public void setNotificationI18n(@NonNull NotificationI18nData data) { // Configure i18n - NotificationDataManager.INSTANCE.setYou(data.getYou()); - NotificationDataManager.INSTANCE.setReply(data.getReply()); - NotificationDataManager.INSTANCE.setMarkAsRead(data.getMarkAsRead()); + NotificationDataManager.INSTANCE.setYou(context, data.getYou()); + NotificationDataManager.INSTANCE.setReply(context, data.getReply()); + NotificationDataManager.INSTANCE.setMarkAsRead(context, data.getMarkAsRead()); } @NonNull 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 800f2e8..b2b6db3 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 @@ -74,7 +74,7 @@ class NotificationReceiver : BroadcastReceiver() { val notification = findActiveNotification(context, id) if (notification == null) { - Log.e(TAG, "Failed to find notification for id ${id}") + Log.e(TAG, "Failed to find notification for id $id") return } @@ -83,7 +83,7 @@ class NotificationReceiver : BroadcastReceiver() { val newStyle = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) Notification.MessagingStyle( android.app.Person.Builder().apply { - setName(NotificationDataManager.you) + setName(NotificationDataManager.getYou(context)) // Set an avatar, if we have one if (NotificationDataManager.avatarPath != null) { @@ -95,7 +95,7 @@ class NotificationReceiver : BroadcastReceiver() { } }.build() ) - else Notification.MessagingStyle(NotificationDataManager.you) + else Notification.MessagingStyle(NotificationDataManager.getYou(context)) newStyle.apply { conversationTitle = recoveredStyle.conversationTitle 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 337a333..377bc8a 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 @@ -14,11 +14,56 @@ import androidx.core.content.FileProvider import androidx.core.graphics.drawable.IconCompat import java.io.File +/* + * Holds "persistent" data for notifications, like i18n strings. While not useful now, this is + * useful for when the app is dead and we receive a notification. + * */ object NotificationDataManager { - var you: String = "You" - var markAsRead: String = "Mark as read" - var reply: String = "Reply" + private var you: String? = null + private var markAsRead: String? = null + private var reply: String? = null var avatarPath: String? = null + + private fun getString(context: Context, key: String, fallback: String): String { + return context.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)!!.getString(key, fallback)!! + } + + private fun setString(context: Context, key: String, value: String) { + val prefs = context.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) + prefs.edit() + .putString(key, value) + .apply() + } + + fun getYou(context: Context): String { + if (you == null) you = getString(context, SHARED_PREFERENCES_YOU_KEY, "You") + return you!! + } + + fun setYou(context: Context, value: String) { + setString(context, SHARED_PREFERENCES_YOU_KEY, value) + you = value + } + + fun getMarkAsRead(context: Context): String { + if (markAsRead == null) markAsRead = getString(context, SHARED_PREFERENCES_MARK_AS_READ_KEY, "Mark as read") + return markAsRead!! + } + + fun setMarkAsRead(context: Context, value: String) { + setString(context, SHARED_PREFERENCES_MARK_AS_READ_KEY, value) + markAsRead = value + } + + fun getReply(context: Context): String { + if (reply != null) reply = getString(context, SHARED_PREFERENCES_REPLY_KEY, "Reply") + return reply!! + } + + fun setReply(context: Context, value: String) { + setString(context, SHARED_PREFERENCES_REPLY_KEY, value) + reply = value + } } /// Show a messaging style notification described by @notification. @@ -26,7 +71,7 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif // Build the actions // -> Reply action val remoteInput = RemoteInput.Builder(REPLY_TEXT_KEY).apply { - setLabel(NotificationDataManager.reply) + setLabel(NotificationDataManager.getReply(context)) }.build() val replyIntent = Intent(context, NotificationReceiver::class.java).apply { action = REPLY_ACTION @@ -41,7 +86,7 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif ) val replyAction = NotificationCompat.Action.Builder( R.drawable.reply, - NotificationDataManager.reply, + NotificationDataManager.getReply(context), replyPendingIntent, ).apply { addRemoteInput(remoteInput) @@ -62,7 +107,7 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif ) val markAsReadAction = NotificationCompat.Action.Builder( R.drawable.mark_as_read, - NotificationDataManager.markAsRead, + NotificationDataManager.getMarkAsRead(context), markAsReadPendingIntent, ).build() @@ -82,7 +127,7 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif // Build the notification val selfPerson = Person.Builder().apply { - setName(NotificationDataManager.you) + setName(NotificationDataManager.getYou(context)) // Set an avatar, if we have one if (NotificationDataManager.avatarPath != null) { From 6da35cd0ba9e3e67a649bcf9f688048aed461f44 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sat, 29 Jul 2023 13:12:41 +0200 Subject: [PATCH 11/23] feat: Allow showing regular notifications --- example/lib/main.dart | 34 ++ .../me/polynom/moxplatform_android/Api.java | 193 +++++++ .../MoxplatformAndroidPlugin.java | 497 +++++++++--------- .../moxplatform_android/Notifications.kt | 16 + .../android/src/main/res/drawable/error.xml | 5 + .../android/src/main/res/drawable/warning.xml | 5 + .../lib/src/notifications_android.dart | 5 + .../lib/src/api.g.dart | 79 +++ .../lib/src/notifications.dart | 3 + .../lib/src/notifications_stub.dart | 4 + pigeons/api.dart | 26 + 11 files changed, 604 insertions(+), 263 deletions(-) create mode 100644 packages/moxplatform_android/android/src/main/res/drawable/error.xml create mode 100644 packages/moxplatform_android/android/src/main/res/drawable/warning.xml diff --git a/example/lib/main.dart b/example/lib/main.dart index b4cd52a..c438a51 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -10,6 +10,7 @@ import 'package:file_picker/file_picker.dart'; /// The id of the notification channel. const channelId = "me.polynom.moxplatform.testing3"; +const otherChannelId = "me.polynom.moxplatform.testing4"; void main() { runApp(const MyApp()); @@ -46,6 +47,11 @@ class MyAppState extends State { channelId, false, ); + await MoxplatformPlugin.notifications.createNotificationChannel( + "Test notification channel for warnings", + otherChannelId, + false, + ); await MoxplatformPlugin.notifications.setI18n( NotificationI18nData( reply: "答える", @@ -190,6 +196,34 @@ class MyHomePage extends StatelessWidget { }, child: const Text('Show messaging notification'), ), + ElevatedButton( + onPressed: () { + MoxplatformPlugin.notifications.showNotification( + RegularNotification( + id: 4384, + title: 'Warning', + body: 'Something brokey', + channelId: otherChannelId, + icon: NotificationIcon.warning, + ), + ); + }, + child: const Text('Show warning notification'), + ), + ElevatedButton( + onPressed: () { + MoxplatformPlugin.notifications.showNotification( + RegularNotification( + id: 4384, + title: 'Error', + body: "Lol, you're on your own", + channelId: otherChannelId, + icon: NotificationIcon.error, + ), + ); + }, + child: const Text('Show error notification'), + ), ElevatedButton( onPressed: () async { final result = await FilePicker.platform.pickFiles( 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 4e910d7..2e62642 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 NotificationIcon { + WARNING(0), + ERROR(1), + NONE(2); + + final int index; + + private NotificationIcon(final int index) { + this.index = index; + } + } + public enum NotificationEventType { MARK_AS_READ(0), REPLY(1), @@ -453,6 +465,156 @@ public class Api { } } + /** Generated class from Pigeon that represents data sent in messages. */ + public static final class RegularNotification { + /** The title of the notification. */ + private @NonNull String title; + + public @NonNull String getTitle() { + return title; + } + + public void setTitle(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"title\" is null."); + } + this.title = setterArg; + } + + /** The body of the notification. */ + private @NonNull String body; + + public @NonNull String getBody() { + return body; + } + + public void setBody(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"body\" is null."); + } + this.body = setterArg; + } + + /** The id of the channel to show the notification on. */ + private @NonNull String channelId; + + public @NonNull String getChannelId() { + return channelId; + } + + public void setChannelId(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"channelId\" is null."); + } + this.channelId = setterArg; + } + + /** The id of the notification. */ + private @NonNull Long id; + + public @NonNull Long getId() { + return id; + } + + public void setId(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"id\" is null."); + } + this.id = setterArg; + } + + /** The icon to use. */ + private @NonNull NotificationIcon icon; + + public @NonNull NotificationIcon getIcon() { + return icon; + } + + public void setIcon(@NonNull NotificationIcon setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"icon\" is null."); + } + this.icon = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + RegularNotification() {} + + public static final class Builder { + + private @Nullable String title; + + public @NonNull Builder setTitle(@NonNull String setterArg) { + this.title = setterArg; + return this; + } + + private @Nullable String body; + + public @NonNull Builder setBody(@NonNull String setterArg) { + this.body = setterArg; + return this; + } + + private @Nullable String channelId; + + public @NonNull Builder setChannelId(@NonNull String setterArg) { + this.channelId = setterArg; + return this; + } + + private @Nullable Long id; + + public @NonNull Builder setId(@NonNull Long setterArg) { + this.id = setterArg; + return this; + } + + private @Nullable NotificationIcon icon; + + public @NonNull Builder setIcon(@NonNull NotificationIcon setterArg) { + this.icon = setterArg; + return this; + } + + public @NonNull RegularNotification build() { + RegularNotification pigeonReturn = new RegularNotification(); + pigeonReturn.setTitle(title); + pigeonReturn.setBody(body); + pigeonReturn.setChannelId(channelId); + pigeonReturn.setId(id); + pigeonReturn.setIcon(icon); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(5); + toListResult.add(title); + toListResult.add(body); + toListResult.add(channelId); + toListResult.add(id); + toListResult.add(icon == null ? null : icon.index); + return toListResult; + } + + static @NonNull RegularNotification fromList(@NonNull ArrayList list) { + RegularNotification pigeonResult = new RegularNotification(); + Object title = list.get(0); + pigeonResult.setTitle((String) title); + Object body = list.get(1); + pigeonResult.setBody((String) body); + Object channelId = list.get(2); + pigeonResult.setChannelId((String) channelId); + Object id = list.get(3); + pigeonResult.setId((id == null) ? null : ((id instanceof Integer) ? (Integer) id : (Long) id)); + Object icon = list.get(4); + pigeonResult.setIcon(icon == null ? null : NotificationIcon.values()[(int) icon]); + return pigeonResult; + } + } + /** Generated class from Pigeon that represents data sent in messages. */ public static final class NotificationEvent { /** The JID the notification was for. */ @@ -672,6 +834,8 @@ public class Api { return NotificationMessage.fromList((ArrayList) readValue(buffer)); case (byte) 132: return NotificationMessageContent.fromList((ArrayList) readValue(buffer)); + case (byte) 133: + return RegularNotification.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); } @@ -694,6 +858,9 @@ public class Api { } else if (value instanceof NotificationMessageContent) { stream.write(132); writeValue(stream, ((NotificationMessageContent) value).toList()); + } else if (value instanceof RegularNotification) { + stream.write(133); + writeValue(stream, ((RegularNotification) value).toList()); } else { super.writeValue(stream, value); } @@ -707,6 +874,8 @@ public class Api { void showMessagingNotification(@NonNull MessagingNotification notification); + void showNotification(@NonNull RegularNotification notification); + void setNotificationSelfAvatar(@NonNull String path); void setNotificationI18n(@NonNull NotificationI18nData data); @@ -765,6 +934,30 @@ public class Api { api.showMessagingNotification(notificationArg); wrapped.add(0, null); } + 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.showNotification", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + RegularNotification notificationArg = (RegularNotification) args.get(0); + try { + api.showNotification(notificationArg); + 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/MoxplatformAndroidPlugin.java b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/MoxplatformAndroidPlugin.java index 599b9bf..ac2f555 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 @@ -4,6 +4,7 @@ import static androidx.core.content.ContextCompat.getSystemService; import static me.polynom.moxplatform_android.ConstantsKt.SHARED_PREFERENCES_KEY; import static me.polynom.moxplatform_android.RecordSentMessageKt.recordSentMessage; import static me.polynom.moxplatform_android.CryptoKt.*; + import me.polynom.moxplatform_android.Api.*; import android.app.ActivityManager; @@ -38,300 +39,270 @@ import io.flutter.plugin.common.JSONMethodCodec; import kotlin.Unit; import kotlin.jvm.functions.Function1; - 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"; - private static final String TAG = "moxplatform_android"; - public static final String methodChannelKey = "me.polynom.moxplatform_android"; - public static final String dataReceivedMethodName = "dataReceived"; +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"; + private static final String TAG = "moxplatform_android"; + public static final String methodChannelKey = "me.polynom.moxplatform_android"; + public static final String dataReceivedMethodName = "dataReceived"; - private static final List _instances = new ArrayList<>(); - private BackgroundService service; - private MethodChannel channel; - private static EventChannel notificationChannel; - public static EventSink notificationSink; + private static final List _instances = new ArrayList<>(); + private BackgroundService service; + private MethodChannel channel; + private static EventChannel notificationChannel; + public static EventSink notificationSink; private Context context; - public MoxplatformAndroidPlugin() { - _instances.add(this); - } - - @Override - public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { - channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), methodChannelKey); - 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)); - - MoxplatformApi.setup(flutterPluginBinding.getBinaryMessenger(), this); - - Log.d(TAG, "Attached to engine"); - } - - static void registerWith(Registrar registrar) { - LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(registrar.context()); - final MoxplatformAndroidPlugin plugin = new MoxplatformAndroidPlugin(); - localBroadcastManager.registerReceiver(plugin, new IntentFilter(methodChannelKey)); - - final MethodChannel channel = new MethodChannel(registrar.messenger(), "me.polynom/background_service_android", JSONMethodCodec.INSTANCE); - channel.setMethodCallHandler(plugin); - plugin.channel = channel; - - 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(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE); - prefs.edit() - .putLong(entrypointKey, entrypointHandle) - .putString(extraDataKey, extraData) - .apply(); - } - - public static long getHandle(Context c) { - return c.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).getLong(entrypointKey, 0); - } - - public static String getExtraData(Context c) { - return c.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).getString(extraDataKey, ""); - } - - public static void setStartAtBoot(Context c, boolean value) { - c.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) - .edit() - .putBoolean(autoStartAtBootKey, value) - .apply(); - } - public static boolean getStartAtBoot(Context c) { - return c.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).getBoolean(autoStartAtBootKey, false); - } - - private boolean isRunning() { - ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - for (ActivityManager.RunningServiceInfo info : manager.getRunningServices(Integer.MAX_VALUE)) { - if (BackgroundService.class.getName().equals(info.service.getClassName())) { - return true; - } + public MoxplatformAndroidPlugin() { + _instances.add(this); } - return false; - } + @Override + public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { + channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), methodChannelKey); + channel.setMethodCallHandler(this); + context = flutterPluginBinding.getApplicationContext(); - @Override - public void onMethodCall(@NonNull MethodCall call, @NonNull io.flutter.plugin.common.MethodChannel.Result result) { - switch (call.method) { - case "configure": - ArrayList args = (ArrayList) call.arguments; - long handle = (long) args.get(0); - String extraData = (String) args.get(1); + notificationChannel = new EventChannel(flutterPluginBinding.getBinaryMessenger(), "me.polynom/notification_stream"); + notificationChannel.setStreamHandler(this); - configure(handle, extraData); - result.success(true); - break; - case "isRunning": - result.success(isRunning()); - break; - case "start": - MoxplatformAndroidPlugin.setStartAtBoot(context, true); - BackgroundService.enqueue(context); - Intent intent = new Intent(context, BackgroundService.class); - ContextCompat.startForegroundService(context, intent); - Log.d(TAG, "Service started"); - result.success(true); - break; - case "sendData": - for (MoxplatformAndroidPlugin plugin : _instances) { - if (plugin.service != null) { - plugin.service.receiveData((String) call.arguments); - break; - } + + LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(this.context); + localBroadcastManager.registerReceiver(this, new IntentFilter(methodChannelKey)); + + MoxplatformApi.setup(flutterPluginBinding.getBinaryMessenger(), this); + + Log.d(TAG, "Attached to engine"); + } + + static void registerWith(Registrar registrar) { + LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(registrar.context()); + final MoxplatformAndroidPlugin plugin = new MoxplatformAndroidPlugin(); + localBroadcastManager.registerReceiver(plugin, new IntentFilter(methodChannelKey)); + + final MethodChannel channel = new MethodChannel(registrar.messenger(), "me.polynom/background_service_android", JSONMethodCodec.INSTANCE); + channel.setMethodCallHandler(plugin); + plugin.channel = channel; + + 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(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE); + prefs.edit().putLong(entrypointKey, entrypointHandle).putString(extraDataKey, extraData).apply(); + } + + public static long getHandle(Context c) { + return c.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).getLong(entrypointKey, 0); + } + + public static String getExtraData(Context c) { + return c.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).getString(extraDataKey, ""); + } + + public static void setStartAtBoot(Context c, boolean value) { + c.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).edit().putBoolean(autoStartAtBootKey, value).apply(); + } + + public static boolean getStartAtBoot(Context c) { + return c.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).getBoolean(autoStartAtBootKey, false); + } + + private boolean isRunning() { + ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + for (ActivityManager.RunningServiceInfo info : manager.getRunningServices(Integer.MAX_VALUE)) { + if (BackgroundService.class.getName().equals(info.service.getClassName())) { + return true; + } } - result.success(true); - break; - case "encryptFile": - Thread encryptionThread = new Thread(new Runnable() { - @Override - public void run() { - ArrayList args = (ArrayList) call.arguments; - String src = (String) args.get(0); - String dest = (String) args.get(1); - byte[] key = (byte[]) args.get(2); - byte[] iv = (byte[]) args.get(3); - int algorithm = (int) args.get(4); - String hashSpec = (String) args.get(5); - result.success( - encryptAndHash( - src, - dest, - key, - iv, - getCipherSpecFromInteger(algorithm), - hashSpec - ) - ); - } - }); - encryptionThread.start(); - break; - case "decryptFile": - Thread decryptionThread = new Thread(new Runnable() { - @Override - public void run() { - ArrayList args = (ArrayList) call.arguments; - String src = (String) args.get(0); - String dest = (String) args.get(1); - byte[] key = (byte[]) args.get(2); - byte[] iv = (byte[]) args.get(3); - int algorithm = (int) args.get(4); - String hashSpec = (String) args.get(5); - - result.success( - decryptAndHash( - src, - dest, - key, - iv, - getCipherSpecFromInteger(algorithm), - hashSpec - ) - ); - } - }); - decryptionThread.start(); - break; - case "hashFile": - Thread hashingThread = new Thread(new Runnable() { - @Override - public void run() { - ArrayList args = (ArrayList) call.arguments; - String src = (String) args.get(0); - String hashSpec = (String) args.get(1); - - result.success(hashFile(src, hashSpec)); - } - }); - hashingThread.start(); - break; - case "recordSentMessage": - ArrayList rargs = (ArrayList) call.arguments; - recordSentMessage( - context, - (String) rargs.get(0), - (String) rargs.get(1), - (String) rargs.get(2), - (int) rargs.get(3) - ); - result.success(true); - break; - default: - result.notImplemented(); - break; + return false; } - } - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction() == null) return; + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull io.flutter.plugin.common.MethodChannel.Result result) { + switch (call.method) { + case "configure": + ArrayList args = (ArrayList) call.arguments; + long handle = (long) args.get(0); + String extraData = (String) args.get(1); - if (intent.getAction().equalsIgnoreCase(methodChannelKey)) { - String data = intent.getStringExtra("data"); + configure(handle, extraData); + result.success(true); + break; + case "isRunning": + result.success(isRunning()); + break; + case "start": + MoxplatformAndroidPlugin.setStartAtBoot(context, true); + BackgroundService.enqueue(context); + Intent intent = new Intent(context, BackgroundService.class); + ContextCompat.startForegroundService(context, intent); + Log.d(TAG, "Service started"); + result.success(true); + break; + case "sendData": + for (MoxplatformAndroidPlugin plugin : _instances) { + if (plugin.service != null) { + plugin.service.receiveData((String) call.arguments); + break; + } + } + result.success(true); + break; + case "encryptFile": + Thread encryptionThread = new Thread(new Runnable() { + @Override + public void run() { + ArrayList args = (ArrayList) call.arguments; + String src = (String) args.get(0); + String dest = (String) args.get(1); + byte[] key = (byte[]) args.get(2); + byte[] iv = (byte[]) args.get(3); + int algorithm = (int) args.get(4); + String hashSpec = (String) args.get(5); - if (channel != null) { - channel.invokeMethod(dataReceivedMethodName, data); - } + result.success(encryptAndHash(src, dest, key, iv, getCipherSpecFromInteger(algorithm), hashSpec)); + } + }); + encryptionThread.start(); + break; + case "decryptFile": + Thread decryptionThread = new Thread(new Runnable() { + @Override + public void run() { + ArrayList args = (ArrayList) call.arguments; + String src = (String) args.get(0); + String dest = (String) args.get(1); + byte[] key = (byte[]) args.get(2); + byte[] iv = (byte[]) args.get(3); + int algorithm = (int) args.get(4); + String hashSpec = (String) args.get(5); + + result.success(decryptAndHash(src, dest, key, iv, getCipherSpecFromInteger(algorithm), hashSpec)); + } + }); + decryptionThread.start(); + break; + case "hashFile": + Thread hashingThread = new Thread(new Runnable() { + @Override + public void run() { + ArrayList args = (ArrayList) call.arguments; + String src = (String) args.get(0); + String hashSpec = (String) args.get(1); + + result.success(hashFile(src, hashSpec)); + } + }); + hashingThread.start(); + break; + case "recordSentMessage": + ArrayList rargs = (ArrayList) call.arguments; + recordSentMessage(context, (String) rargs.get(0), (String) rargs.get(1), (String) rargs.get(2), (int) rargs.get(3)); + result.success(true); + break; + default: + result.notImplemented(); + break; + } } - } - @Override - public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { - channel.setMethodCallHandler(null); - LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(this.context); - localBroadcastManager.unregisterReceiver(this); + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction() == null) return; - Log.d(TAG, "Detached from engine"); - } + if (intent.getAction().equalsIgnoreCase(methodChannelKey)) { + String data = intent.getStringExtra("data"); - @Override - public void onAttachedToService(@NonNull ServicePluginBinding binding) { - Log.d(TAG, "Attached to service"); - this.service = (BackgroundService) binding.getService(); - } + if (channel != null) { + channel.invokeMethod(dataReceivedMethodName, data); + } + } + } - @Override - public void onDetachedFromService() { - Log.d(TAG, "Detached from service"); - this.service = null; - } + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + channel.setMethodCallHandler(null); + LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(this.context); + localBroadcastManager.unregisterReceiver(this); - @Override - public void createNotificationChannel(@NonNull String title, @NonNull String id, @NonNull Boolean urgent) { - final NotificationChannel channel = new NotificationChannel( - id, - title, - urgent ? NotificationManager.IMPORTANCE_HIGH : NotificationManager.IMPORTANCE_DEFAULT - ); - channel.enableVibration(true); - channel.enableLights(true); - final NotificationManager manager = getSystemService(context, NotificationManager.class); - manager.createNotificationChannel(channel); - } + Log.d(TAG, "Detached from engine"); + } - @Override - public void showMessagingNotification(@NonNull MessagingNotification notification) { - NotificationsKt.showMessagingNotification(context, notification); - } + @Override + public void onAttachedToService(@NonNull ServicePluginBinding binding) { + Log.d(TAG, "Attached to service"); + this.service = (BackgroundService) binding.getService(); + } + + @Override + public void onDetachedFromService() { + Log.d(TAG, "Detached from service"); + this.service = null; + } + + @Override + public void createNotificationChannel(@NonNull String title, @NonNull String id, @NonNull Boolean urgent) { + final NotificationChannel channel = new NotificationChannel(id, title, urgent ? NotificationManager.IMPORTANCE_HIGH : NotificationManager.IMPORTANCE_DEFAULT); + channel.enableVibration(true); + channel.enableLights(true); + final NotificationManager manager = getSystemService(context, NotificationManager.class); + manager.createNotificationChannel(channel); + } + + @Override + public void showMessagingNotification(@NonNull MessagingNotification notification) { + NotificationsKt.showMessagingNotification(context, notification); + } + + @Override + public void showNotification(@NonNull RegularNotification notification) { + NotificationsKt.showNotification(context, notification); + } @Override public void setNotificationSelfAvatar(@NonNull String path) { - NotificationDataManager.INSTANCE.setAvatarPath(path); + NotificationDataManager.INSTANCE.setAvatarPath(path); } @Override public void setNotificationI18n(@NonNull NotificationI18nData data) { - // Configure i18n - NotificationDataManager.INSTANCE.setYou(context, data.getYou()); - NotificationDataManager.INSTANCE.setReply(context, data.getReply()); - NotificationDataManager.INSTANCE.setMarkAsRead(context, data.getMarkAsRead()); + // Configure i18n + NotificationDataManager.INSTANCE.setYou(context, data.getYou()); + NotificationDataManager.INSTANCE.setReply(context, data.getReply()); + NotificationDataManager.INSTANCE.setMarkAsRead(context, data.getMarkAsRead()); } @NonNull - @Override - public String getPersistentDataPath() { - return context.getFilesDir().getPath(); - } + @Override + public String getPersistentDataPath() { + return context.getFilesDir().getPath(); + } - @NonNull - @Override - public String getCacheDataPath() { - return context.getCacheDir().getPath(); - } + @NonNull + @Override + public String getCacheDataPath() { + return context.getCacheDir().getPath(); + } - @Override - public void eventStub(@NonNull NotificationEvent event) { - // Stub to trick pigeon into - } + @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/Notifications.kt b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Notifications.kt index 377bc8a..3b89ca1 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 @@ -211,4 +211,20 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif notification.id.toInt(), finalNotification, ) +} + +fun showNotification(context: Context, notification: Api.RegularNotification) { + val builtNotification = NotificationCompat.Builder(context, notification.channelId).apply { + setContentTitle(notification.title) + setContentText(notification.body) + + when (notification.icon) { + Api.NotificationIcon.ERROR -> setSmallIcon(R.drawable.error) + Api.NotificationIcon.WARNING -> setSmallIcon(R.drawable.warning) + Api.NotificationIcon.NONE -> {} + } + }.build() + + // Post the notification + NotificationManagerCompat.from(context).notify(notification.id.toInt(), builtNotification) } \ No newline at end of file diff --git a/packages/moxplatform_android/android/src/main/res/drawable/error.xml b/packages/moxplatform_android/android/src/main/res/drawable/error.xml new file mode 100644 index 0000000..1757571 --- /dev/null +++ b/packages/moxplatform_android/android/src/main/res/drawable/error.xml @@ -0,0 +1,5 @@ + + + diff --git a/packages/moxplatform_android/android/src/main/res/drawable/warning.xml b/packages/moxplatform_android/android/src/main/res/drawable/warning.xml new file mode 100644 index 0000000..3c9a4b3 --- /dev/null +++ b/packages/moxplatform_android/android/src/main/res/drawable/warning.xml @@ -0,0 +1,5 @@ + + + diff --git a/packages/moxplatform_android/lib/src/notifications_android.dart b/packages/moxplatform_android/lib/src/notifications_android.dart index 7fd95fa..9f3494d 100644 --- a/packages/moxplatform_android/lib/src/notifications_android.dart +++ b/packages/moxplatform_android/lib/src/notifications_android.dart @@ -24,6 +24,11 @@ class AndroidNotificationsImplementation extends NotificationsImplementation { return _api.showMessagingNotification(notification); } + @override + Future showNotification(RegularNotification notification) async { + return _api.showNotification(notification); + } + @override Future setNotificationSelfAvatar(String path) async { return _api.setNotificationSelfAvatar(path); diff --git a/packages/moxplatform_platform_interface/lib/src/api.g.dart b/packages/moxplatform_platform_interface/lib/src/api.g.dart index d81daaa..36fd541 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 NotificationIcon { + warning, + error, + none, +} + enum NotificationEventType { markAsRead, reply, @@ -139,6 +145,52 @@ class MessagingNotification { } } +class RegularNotification { + RegularNotification({ + required this.title, + required this.body, + required this.channelId, + required this.id, + required this.icon, + }); + + /// The title of the notification. + String title; + + /// The body of the notification. + String body; + + /// The id of the channel to show the notification on. + String channelId; + + /// The id of the notification. + int id; + + /// The icon to use. + NotificationIcon icon; + + Object encode() { + return [ + title, + body, + channelId, + id, + icon.index, + ]; + } + + static RegularNotification decode(Object result) { + result as List; + return RegularNotification( + title: result[0]! as String, + body: result[1]! as String, + channelId: result[2]! as String, + id: result[3]! as int, + icon: NotificationIcon.values[result[4]! as int], + ); + } +} + class NotificationEvent { NotificationEvent({ required this.jid, @@ -228,6 +280,9 @@ class _MoxplatformApiCodec extends StandardMessageCodec { } else if (value is NotificationMessageContent) { buffer.putUint8(132); writeValue(buffer, value.encode()); + } else if (value is RegularNotification) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -246,6 +301,8 @@ class _MoxplatformApiCodec extends StandardMessageCodec { return NotificationMessage.decode(readValue(buffer)!); case 132: return NotificationMessageContent.decode(readValue(buffer)!); + case 133: + return RegularNotification.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -306,6 +363,28 @@ class MoxplatformApi { } } + Future showNotification(RegularNotification arg_notification) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.showNotification', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_notification]) 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 setNotificationSelfAvatar(String arg_path) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.setNotificationSelfAvatar', codec, diff --git a/packages/moxplatform_platform_interface/lib/src/notifications.dart b/packages/moxplatform_platform_interface/lib/src/notifications.dart index 31ef838..2c69bf5 100644 --- a/packages/moxplatform_platform_interface/lib/src/notifications.dart +++ b/packages/moxplatform_platform_interface/lib/src/notifications.dart @@ -9,6 +9,9 @@ abstract class NotificationsImplementation { /// Shows a notification [notification] in the messaging style with everyting it needs. Future showMessagingNotification(MessagingNotification notification); + /// Shows a regular notification [notification]. + Future showNotification(RegularNotification notification); + /// Sets the path to the self-avatar for in-notification replies. Future setNotificationSelfAvatar(String path); diff --git a/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart b/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart index 2bc36fe..d847a1e 100644 --- a/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart +++ b/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart @@ -9,6 +9,10 @@ class StubNotificationsImplementation extends NotificationsImplementation { @override Future showMessagingNotification(MessagingNotification notification) async {} + @override + Future showNotification(RegularNotification notification) async {} + + @override Future setNotificationSelfAvatar(String path) async {} diff --git a/pigeons/api.dart b/pigeons/api.dart index d1877fc..a156cef 100644 --- a/pigeons/api.dart +++ b/pigeons/api.dart @@ -72,6 +72,31 @@ class MessagingNotification { final List messages; } +enum NotificationIcon { + warning, + error, + none, +} + +class RegularNotification { + const RegularNotification(this.title, this.body, this.channelId, this.id, this.icon); + + /// The title of the notification. + final String title; + + /// The body of the notification. + final String body; + + /// The id of the channel to show the notification on. + final String channelId; + + /// The id of the notification. + final int id; + + /// The icon to use. + final NotificationIcon icon; +} + enum NotificationEventType { markAsRead, reply, @@ -114,6 +139,7 @@ class NotificationI18nData { abstract class MoxplatformApi { void createNotificationChannel(String title, String id, bool urgent); void showMessagingNotification(MessagingNotification notification); + void showNotification(RegularNotification notification); void setNotificationSelfAvatar(String path); void setNotificationI18n(NotificationI18nData data); String getPersistentDataPath(); From c7ee2b6c6e187e0b232aa0405b953b7cf14af797 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sat, 29 Jul 2023 15:32:33 +0200 Subject: [PATCH 12/23] feat: Allow attaching arbitrary data to the notification --- example/lib/main.dart | 7 ++- .../me/polynom/moxplatform_android/Api.java | 48 ++++++++++++++++++- .../NotificationReceiver.kt | 8 ++++ .../moxplatform_android/Notifications.kt | 5 ++ .../lib/src/api.g.dart | 12 +++++ pigeons/api.dart | 8 +++- 6 files changed, 84 insertions(+), 4 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index c438a51..bc6ea6d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -61,7 +61,7 @@ class MyAppState extends State { ); MoxplatformPlugin.notifications.getEventStream().listen((event) { - print('NotificationEvent(type: ${event.type}, jid: ${event.jid}, payload: ${event.payload})'); + print('NotificationEvent(type: ${event.type}, jid: ${event.jid}, payload: ${event.payload}, extras: ${event.extra})'); }); } @@ -191,6 +191,11 @@ class MyHomePage extends StatelessWidget { messages: messages, channelId: channelId, jid: 'testjid', + extra: { + 'jid': 'testjid', + 'avatarPath': 'lol', + 'rio': 'cute', + }, ), ); }, 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 2e62642..30f3bac 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 @@ -387,6 +387,17 @@ public class Api { this.messages = setterArg; } + /** Additional data to include. */ + private @Nullable Map extra; + + public @Nullable Map getExtra() { + return extra; + } + + public void setExtra(@Nullable Map setterArg) { + this.extra = setterArg; + } + /** Constructor is non-public to enforce null safety; use Builder. */ MessagingNotification() {} @@ -427,6 +438,13 @@ public class Api { return this; } + private @Nullable Map extra; + + public @NonNull Builder setExtra(@Nullable Map setterArg) { + this.extra = setterArg; + return this; + } + public @NonNull MessagingNotification build() { MessagingNotification pigeonReturn = new MessagingNotification(); pigeonReturn.setTitle(title); @@ -434,18 +452,20 @@ public class Api { pigeonReturn.setChannelId(channelId); pigeonReturn.setJid(jid); pigeonReturn.setMessages(messages); + pigeonReturn.setExtra(extra); return pigeonReturn; } } @NonNull ArrayList toList() { - ArrayList toListResult = new ArrayList(5); + ArrayList toListResult = new ArrayList(6); toListResult.add(title); toListResult.add(id); toListResult.add(channelId); toListResult.add(jid); toListResult.add(messages); + toListResult.add(extra); return toListResult; } @@ -461,6 +481,8 @@ public class Api { pigeonResult.setJid((String) jid); Object messages = list.get(4); pigeonResult.setMessages((List) messages); + Object extra = list.get(5); + pigeonResult.setExtra((Map) extra); return pigeonResult; } } @@ -660,6 +682,17 @@ public class Api { this.payload = setterArg; } + /** Extra data. Only set when type == NotificationType.reply. */ + private @Nullable Map extra; + + public @Nullable Map getExtra() { + return extra; + } + + public void setExtra(@Nullable Map setterArg) { + this.extra = setterArg; + } + /** Constructor is non-public to enforce null safety; use Builder. */ NotificationEvent() {} @@ -686,21 +719,30 @@ public class Api { return this; } + private @Nullable Map extra; + + public @NonNull Builder setExtra(@Nullable Map setterArg) { + this.extra = setterArg; + return this; + } + public @NonNull NotificationEvent build() { NotificationEvent pigeonReturn = new NotificationEvent(); pigeonReturn.setJid(jid); pigeonReturn.setType(type); pigeonReturn.setPayload(payload); + pigeonReturn.setExtra(extra); return pigeonReturn; } } @NonNull ArrayList toList() { - ArrayList toListResult = new ArrayList(3); + ArrayList toListResult = new ArrayList(4); toListResult.add(jid); toListResult.add(type == null ? null : type.index); toListResult.add(payload); + toListResult.add(extra); return toListResult; } @@ -712,6 +754,8 @@ public class Api { pigeonResult.setType(type == null ? null : NotificationEventType.values()[(int) type]); Object payload = list.get(2); pigeonResult.setPayload((String) payload); + Object extra = list.get(3); + pigeonResult.setExtra((Map) extra); return pigeonResult; } } 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 b2b6db3..3ddeb9f 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 @@ -148,11 +148,19 @@ class NotificationReceiver : BroadcastReceiver() { } private fun handleTap(context: Context, intent: Intent) { + val extras = mutableMapOf() + intent.extras?.keySet()!!.forEach { + if (it.startsWith("payload_")) { + extras[it.substring(8)] = intent.extras!!.getString(it) + } + } + MoxplatformAndroidPlugin.notificationSink?.success( NotificationEvent().apply { jid = intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!! type = Api.NotificationEventType.OPEN payload = null + extra = extras }.toList() ) 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 3b89ca1..eafa1b9 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 @@ -6,6 +6,7 @@ import android.content.Context import android.content.Intent import android.graphics.BitmapFactory import android.graphics.Color +import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.Person @@ -117,6 +118,10 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif action = TAP_ACTION putExtra(NOTIFICATION_EXTRA_JID_KEY, notification.jid) putExtra(NOTIFICATION_EXTRA_ID_KEY, notification.id) + + notification.extra?.forEach { + putExtra("payload_${it.key}", it.value) + } } val tapPendingIntent = PendingIntent.getBroadcast( context, diff --git a/packages/moxplatform_platform_interface/lib/src/api.g.dart b/packages/moxplatform_platform_interface/lib/src/api.g.dart index 36fd541..fd4b2d0 100644 --- a/packages/moxplatform_platform_interface/lib/src/api.g.dart +++ b/packages/moxplatform_platform_interface/lib/src/api.g.dart @@ -106,6 +106,7 @@ class MessagingNotification { required this.channelId, required this.jid, required this.messages, + this.extra, }); /// The title of the conversation. @@ -123,6 +124,9 @@ class MessagingNotification { /// Messages to show. List messages; + /// Additional data to include. + Map? extra; + Object encode() { return [ title, @@ -130,6 +134,7 @@ class MessagingNotification { channelId, jid, messages, + extra, ]; } @@ -141,6 +146,7 @@ class MessagingNotification { channelId: result[2]! as String, jid: result[3]! as String, messages: (result[4] as List?)!.cast(), + extra: (result[5] as Map?)?.cast(), ); } } @@ -196,6 +202,7 @@ class NotificationEvent { required this.jid, required this.type, this.payload, + this.extra, }); /// The JID the notification was for. @@ -209,11 +216,15 @@ class NotificationEvent { /// Otherwise: undefined. String? payload; + /// Extra data. Only set when type == NotificationType.reply. + Map? extra; + Object encode() { return [ jid, type.index, payload, + extra, ]; } @@ -223,6 +234,7 @@ class NotificationEvent { jid: result[0]! as String, type: NotificationEventType.values[result[1]! as int], payload: result[2] as String?, + extra: (result[3] as Map?)?.cast(), ); } } diff --git a/pigeons/api.dart b/pigeons/api.dart index a156cef..33ae13b 100644 --- a/pigeons/api.dart +++ b/pigeons/api.dart @@ -54,7 +54,7 @@ class NotificationMessage { } class MessagingNotification { - const MessagingNotification(this.title, this.id, this.jid, this.messages, this.channelId); + const MessagingNotification(this.title, this.id, this.jid, this.messages, this.channelId, this.extra); /// The title of the conversation. final String title; @@ -70,6 +70,9 @@ class MessagingNotification { /// Messages to show. final List messages; + + /// Additional data to include. + final Map? extra; } enum NotificationIcon { @@ -120,6 +123,9 @@ class NotificationEvent { /// - type == NotificationType.reply: The reply message text. /// Otherwise: undefined. final String? payload; + + /// Extra data. Only set when type == NotificationType.reply. + final Map? extra; } class NotificationI18nData { From 2490a8ee9f1efe401b0e532f3749bf151580cc6f Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sun, 30 Jul 2023 20:40:59 +0200 Subject: [PATCH 13/23] fix: Add payload to all intents --- example/lib/main.dart | 3 + .../me/polynom/moxplatform_android/Api.java | 64 +++++++++++++++++-- .../MoxplatformAndroidPlugin.java | 13 +++- .../NotificationReceiver.kt | 26 +++++--- .../moxplatform_android/Notifications.kt | 37 +++++++++-- .../lib/src/notifications_android.dart | 8 ++- .../lib/src/api.g.dart | 34 +++++++++- .../lib/src/notifications.dart | 5 +- .../lib/src/notifications_stub.dart | 4 +- pigeons/api.dart | 9 ++- 10 files changed, 171 insertions(+), 32 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index bc6ea6d..aabae91 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -44,11 +44,13 @@ class MyAppState extends State { await MoxplatformPlugin.notifications.createNotificationChannel( "Test notification channel", + "Test1", channelId, false, ); await MoxplatformPlugin.notifications.createNotificationChannel( "Test notification channel for warnings", + "Test2", otherChannelId, false, ); @@ -191,6 +193,7 @@ class MyHomePage extends StatelessWidget { messages: messages, channelId: channelId, jid: 'testjid', + isGroupchat: true, extra: { 'jid': 'testjid', 'avatarPath': 'lol', 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 30f3bac..1a18664 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 @@ -387,6 +387,20 @@ public class Api { this.messages = setterArg; } + /** Flag indicating whether this notification is from a groupchat or not. */ + private @NonNull Boolean isGroupchat; + + public @NonNull Boolean getIsGroupchat() { + return isGroupchat; + } + + public void setIsGroupchat(@NonNull Boolean setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"isGroupchat\" is null."); + } + this.isGroupchat = setterArg; + } + /** Additional data to include. */ private @Nullable Map extra; @@ -438,6 +452,13 @@ public class Api { return this; } + private @Nullable Boolean isGroupchat; + + public @NonNull Builder setIsGroupchat(@NonNull Boolean setterArg) { + this.isGroupchat = setterArg; + return this; + } + private @Nullable Map extra; public @NonNull Builder setExtra(@Nullable Map setterArg) { @@ -452,6 +473,7 @@ public class Api { pigeonReturn.setChannelId(channelId); pigeonReturn.setJid(jid); pigeonReturn.setMessages(messages); + pigeonReturn.setIsGroupchat(isGroupchat); pigeonReturn.setExtra(extra); return pigeonReturn; } @@ -459,12 +481,13 @@ public class Api { @NonNull ArrayList toList() { - ArrayList toListResult = new ArrayList(6); + ArrayList toListResult = new ArrayList(7); toListResult.add(title); toListResult.add(id); toListResult.add(channelId); toListResult.add(jid); toListResult.add(messages); + toListResult.add(isGroupchat); toListResult.add(extra); return toListResult; } @@ -481,7 +504,9 @@ public class Api { pigeonResult.setJid((String) jid); Object messages = list.get(4); pigeonResult.setMessages((List) messages); - Object extra = list.get(5); + Object isGroupchat = list.get(5); + pigeonResult.setIsGroupchat((Boolean) isGroupchat); + Object extra = list.get(6); pigeonResult.setExtra((Map) extra); return pigeonResult; } @@ -914,12 +939,14 @@ public class Api { /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface MoxplatformApi { - void createNotificationChannel(@NonNull String title, @NonNull String id, @NonNull Boolean urgent); + void createNotificationChannel(@NonNull String title, @NonNull String description, @NonNull String id, @NonNull Boolean urgent); void showMessagingNotification(@NonNull MessagingNotification notification); void showNotification(@NonNull RegularNotification notification); + void dismissNotification(@NonNull Long id); + void setNotificationSelfAvatar(@NonNull String path); void setNotificationI18n(@NonNull NotificationI18nData data); @@ -948,10 +975,11 @@ public class Api { ArrayList wrapped = new ArrayList(); ArrayList args = (ArrayList) message; String titleArg = (String) args.get(0); - String idArg = (String) args.get(1); - Boolean urgentArg = (Boolean) args.get(2); + String descriptionArg = (String) args.get(1); + String idArg = (String) args.get(2); + Boolean urgentArg = (Boolean) args.get(3); try { - api.createNotificationChannel(titleArg, idArg, urgentArg); + api.createNotificationChannel(titleArg, descriptionArg, idArg, urgentArg); wrapped.add(0, null); } catch (Throwable exception) { @@ -1002,6 +1030,30 @@ public class Api { api.showNotification(notificationArg); wrapped.add(0, null); } + 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.dismissNotification", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number idArg = (Number) args.get(0); + try { + api.dismissNotification((idArg == null) ? null : idArg.longValue()); + 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/MoxplatformAndroidPlugin.java b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/MoxplatformAndroidPlugin.java index ac2f555..03833ed 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 @@ -2,8 +2,8 @@ package me.polynom.moxplatform_android; import static androidx.core.content.ContextCompat.getSystemService; import static me.polynom.moxplatform_android.ConstantsKt.SHARED_PREFERENCES_KEY; -import static me.polynom.moxplatform_android.RecordSentMessageKt.recordSentMessage; import static me.polynom.moxplatform_android.CryptoKt.*; +import static me.polynom.moxplatform_android.RecordSentMessageKt.*; import me.polynom.moxplatform_android.Api.*; @@ -18,6 +18,7 @@ import android.content.SharedPreferences; import android.util.Log; import androidx.annotation.NonNull; +import androidx.core.app.NotificationManagerCompat; import androidx.core.content.ContextCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; @@ -39,7 +40,7 @@ import io.flutter.plugin.common.JSONMethodCodec; import kotlin.Unit; import kotlin.jvm.functions.Function1; -public class MoxplatformAndroidPlugin extends BroadcastReceiver implements FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler, 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"; @@ -258,10 +259,11 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt } @Override - public void createNotificationChannel(@NonNull String title, @NonNull String id, @NonNull Boolean urgent) { + public void createNotificationChannel(@NonNull String title, @NonNull String description, @NonNull String id, @NonNull Boolean urgent) { final NotificationChannel channel = new NotificationChannel(id, title, urgent ? NotificationManager.IMPORTANCE_HIGH : NotificationManager.IMPORTANCE_DEFAULT); channel.enableVibration(true); channel.enableLights(true); + channel.setDescription(description); final NotificationManager manager = getSystemService(context, NotificationManager.class); manager.createNotificationChannel(channel); } @@ -276,6 +278,11 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt NotificationsKt.showNotification(context, notification); } + @Override + public void dismissNotification(@NonNull Long id) { + NotificationManagerCompat.from(context).cancel(id.intValue()); + } + @Override public void setNotificationSelfAvatar(@NonNull String path) { NotificationDataManager.INSTANCE.setAvatarPath(path); 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 3ddeb9f..f608e16 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 @@ -42,16 +42,30 @@ class NotificationReceiver : BroadcastReceiver() { .find { it.id == id }?.notification } + private fun extractPayloadMapFromIntent(intent: Intent): Map { + val extras = mutableMapOf() + intent.extras?.keySet()!!.forEach { + Log.d(TAG, "Checking $it -> ${intent.extras!!.get(it)}") + if (it.startsWith("payload_")) { + Log.d(TAG, "Adding $it") + extras[it.substring(8)] = intent.extras!!.getString(it) + } + } + + return extras + } + private fun handleMarkAsRead(context: Context, intent: Intent) { - NotificationManagerCompat.from(context).cancel(intent.getLongExtra(MARK_AS_READ_ID_KEY, -1).toInt()) MoxplatformAndroidPlugin.notificationSink?.success( NotificationEvent().apply { jid = intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!! type = Api.NotificationEventType.MARK_AS_READ payload = null + extra = extractPayloadMapFromIntent(intent) }.toList() ) + NotificationManagerCompat.from(context).cancel(intent.getLongExtra(MARK_AS_READ_ID_KEY, -1).toInt()) dismissNotification(context, intent); } @@ -63,6 +77,7 @@ class NotificationReceiver : BroadcastReceiver() { jid = intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!! type = Api.NotificationEventType.REPLY payload = replyPayload.toString() + extra = extractPayloadMapFromIntent(intent) }.toList() ) @@ -148,19 +163,12 @@ class NotificationReceiver : BroadcastReceiver() { } private fun handleTap(context: Context, intent: Intent) { - val extras = mutableMapOf() - intent.extras?.keySet()!!.forEach { - if (it.startsWith("payload_")) { - extras[it.substring(8)] = intent.extras!!.getString(it) - } - } - MoxplatformAndroidPlugin.notificationSink?.success( NotificationEvent().apply { jid = intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!! type = Api.NotificationEventType.OPEN payload = null - extra = extras + extra = extractPayloadMapFromIntent(intent) }.toList() ) 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 eafa1b9..083575f 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 @@ -14,6 +14,7 @@ import androidx.core.app.RemoteInput import androidx.core.content.FileProvider import androidx.core.graphics.drawable.IconCompat import java.io.File +import java.lang.Exception /* * Holds "persistent" data for notifications, like i18n strings. While not useful now, this is @@ -78,6 +79,10 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif action = REPLY_ACTION putExtra(NOTIFICATION_EXTRA_JID_KEY, notification.jid) putExtra(NOTIFICATION_EXTRA_ID_KEY, notification.id) + + notification.extra?.forEach { + putExtra("payload_${it.key}", it.value) + } } val replyPendingIntent = PendingIntent.getBroadcast( context.applicationContext, @@ -99,6 +104,10 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif action = MARK_AS_READ_ACTION putExtra(NOTIFICATION_EXTRA_JID_KEY, notification.jid) putExtra(NOTIFICATION_EXTRA_ID_KEY, notification.id) + + notification.extra?.forEach { + putExtra("payload_${it.key}", it.value) + } } val markAsReadPendingIntent = PendingIntent.getBroadcast( context.applicationContext, @@ -144,7 +153,14 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif } }.build() val style = NotificationCompat.MessagingStyle(selfPerson); - for (message in notification.messages) { + style.isGroupConversation = notification.isGroupchat + if (notification.isGroupchat) { + style.conversationTitle = notification.title + } + + for (i in notification.messages.indices) { + val message = notification.messages[i] + // Build the sender val sender = Person.Builder().apply { setName(message.sender) @@ -152,11 +168,15 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif // Set the avatar, if available if (message.avatarPath != null) { - setIcon( - IconCompat.createWithAdaptiveBitmap( - BitmapFactory.decodeFile(message.avatarPath), - ), - ) + try { + setIcon( + IconCompat.createWithAdaptiveBitmap( + BitmapFactory.decodeFile(message.avatarPath), + ), + ) + } catch (ex: Throwable) { + Log.w(TAG, "Failed to open avatar at ${message.avatarPath}") + } } }.build() @@ -204,6 +224,11 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif addAction(replyAction) addAction(markAsReadAction) + // Groupchat title + if (notification.isGroupchat) { + setContentTitle(notification.title) + } + setAllowSystemGeneratedContextualActions(true) setCategory(Notification.CATEGORY_MESSAGE) diff --git a/packages/moxplatform_android/lib/src/notifications_android.dart b/packages/moxplatform_android/lib/src/notifications_android.dart index 9f3494d..acd0610 100644 --- a/packages/moxplatform_android/lib/src/notifications_android.dart +++ b/packages/moxplatform_android/lib/src/notifications_android.dart @@ -11,10 +11,11 @@ class AndroidNotificationsImplementation extends NotificationsImplementation { @override Future createNotificationChannel( String title, + String description, String id, bool urgent, ) async { - return _api.createNotificationChannel(title, id, urgent); + return _api.createNotificationChannel(title, description, id, urgent); } @override @@ -29,6 +30,11 @@ class AndroidNotificationsImplementation extends NotificationsImplementation { return _api.showNotification(notification); } + @override + Future dismissNotification(int id) async { + return _api.dismissNotification(id); + } + @override Future setNotificationSelfAvatar(String path) async { return _api.setNotificationSelfAvatar(path); diff --git a/packages/moxplatform_platform_interface/lib/src/api.g.dart b/packages/moxplatform_platform_interface/lib/src/api.g.dart index fd4b2d0..67246d7 100644 --- a/packages/moxplatform_platform_interface/lib/src/api.g.dart +++ b/packages/moxplatform_platform_interface/lib/src/api.g.dart @@ -106,6 +106,7 @@ class MessagingNotification { required this.channelId, required this.jid, required this.messages, + required this.isGroupchat, this.extra, }); @@ -124,6 +125,9 @@ class MessagingNotification { /// Messages to show. List messages; + /// Flag indicating whether this notification is from a groupchat or not. + bool isGroupchat; + /// Additional data to include. Map? extra; @@ -134,6 +138,7 @@ class MessagingNotification { channelId, jid, messages, + isGroupchat, extra, ]; } @@ -146,7 +151,8 @@ class MessagingNotification { channelId: result[2]! as String, jid: result[3]! as String, messages: (result[4] as List?)!.cast(), - extra: (result[5] as Map?)?.cast(), + isGroupchat: result[5]! as bool, + extra: (result[6] as Map?)?.cast(), ); } } @@ -331,12 +337,12 @@ class MoxplatformApi { static const MessageCodec codec = _MoxplatformApiCodec(); - Future createNotificationChannel(String arg_title, String arg_id, bool arg_urgent) async { + Future createNotificationChannel(String arg_title, String arg_description, String arg_id, bool arg_urgent) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.createNotificationChannel', codec, binaryMessenger: _binaryMessenger); final List? replyList = - await channel.send([arg_title, arg_id, arg_urgent]) as List?; + await channel.send([arg_title, arg_description, arg_id, arg_urgent]) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -397,6 +403,28 @@ class MoxplatformApi { } } + Future dismissNotification(int arg_id) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.dismissNotification', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_id]) 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 setNotificationSelfAvatar(String arg_path) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.setNotificationSelfAvatar', codec, diff --git a/packages/moxplatform_platform_interface/lib/src/notifications.dart b/packages/moxplatform_platform_interface/lib/src/notifications.dart index 2c69bf5..3918bed 100644 --- a/packages/moxplatform_platform_interface/lib/src/notifications.dart +++ b/packages/moxplatform_platform_interface/lib/src/notifications.dart @@ -4,7 +4,7 @@ import 'package:moxplatform_platform_interface/src/api.g.dart'; abstract class NotificationsImplementation { /// Creates a notification channel with the name [title] and id [id]. If [urgent] is true, then /// it configures the channel as carrying urgent information. - Future createNotificationChannel(String title, String id, bool urgent); + Future createNotificationChannel(String title, String description, String id, bool urgent); /// Shows a notification [notification] in the messaging style with everyting it needs. Future showMessagingNotification(MessagingNotification notification); @@ -12,6 +12,9 @@ abstract class NotificationsImplementation { /// Shows a regular notification [notification]. Future showNotification(RegularNotification notification); + /// Dismisses the notification with id [id]. + Future dismissNotification(int id); + /// Sets the path to the self-avatar for in-notification replies. Future setNotificationSelfAvatar(String path); diff --git a/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart b/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart index d847a1e..0fe38d4 100644 --- a/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart +++ b/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart @@ -4,7 +4,7 @@ import 'package:moxplatform_platform_interface/src/notifications.dart'; class StubNotificationsImplementation extends NotificationsImplementation { @override - Future createNotificationChannel(String title, String id, bool urgent) async {} + Future createNotificationChannel(String title, String description, String id, bool urgent) async {} @override Future showMessagingNotification(MessagingNotification notification) async {} @@ -12,6 +12,8 @@ class StubNotificationsImplementation extends NotificationsImplementation { @override Future showNotification(RegularNotification notification) async {} + @override + Future dismissNotification(int id) async {} @override Future setNotificationSelfAvatar(String path) async {} diff --git a/pigeons/api.dart b/pigeons/api.dart index 33ae13b..f68f400 100644 --- a/pigeons/api.dart +++ b/pigeons/api.dart @@ -54,7 +54,7 @@ class NotificationMessage { } class MessagingNotification { - const MessagingNotification(this.title, this.id, this.jid, this.messages, this.channelId, this.extra); + const MessagingNotification(this.title, this.id, this.jid, this.messages, this.channelId, this.isGroupchat, this.extra); /// The title of the conversation. final String title; @@ -71,6 +71,9 @@ class MessagingNotification { /// Messages to show. final List messages; + /// Flag indicating whether this notification is from a groupchat or not. + final bool isGroupchat; + /// Additional data to include. final Map? extra; } @@ -111,6 +114,7 @@ class NotificationEvent { this.jid, this.type, this.payload, + this.extra, ); /// The JID the notification was for. @@ -143,9 +147,10 @@ class NotificationI18nData { @HostApi() abstract class MoxplatformApi { - void createNotificationChannel(String title, String id, bool urgent); + void createNotificationChannel(String title, String description, String id, bool urgent); void showMessagingNotification(MessagingNotification notification); void showNotification(RegularNotification notification); + void dismissNotification(int id); void setNotificationSelfAvatar(String path); void setNotificationI18n(NotificationI18nData data); String getPersistentDataPath(); From 2f5a39416bd328f75aa9fd18b30e1dffdd9d9785 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sun, 30 Jul 2023 22:05:45 +0200 Subject: [PATCH 14/23] feat: Allow the sender's data being null --- .../me/polynom/moxplatform_android/Api.java | 57 ++++++++++++------- .../NotificationReceiver.kt | 3 + .../lib/src/api.g.dart | 26 +++++---- pigeons/api.dart | 8 ++- 4 files changed, 63 insertions(+), 31 deletions(-) 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 1a18664..20bf705 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 @@ -171,30 +171,24 @@ public class Api { /** Generated class from Pigeon that represents data sent in messages. */ public static final class NotificationMessage { /** The sender of the message. */ - private @NonNull String sender; + private @Nullable String sender; - public @NonNull String getSender() { + public @Nullable String getSender() { return sender; } - public void setSender(@NonNull String setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"sender\" is null."); - } + public void setSender(@Nullable String setterArg) { this.sender = setterArg; } /** The jid of the sender. */ - private @NonNull String jid; + private @Nullable String jid; - public @NonNull String getJid() { + public @Nullable String getJid() { return jid; } - public void setJid(@NonNull String setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"jid\" is null."); - } + public void setJid(@Nullable String setterArg) { this.jid = setterArg; } @@ -244,14 +238,14 @@ public class Api { private @Nullable String sender; - public @NonNull Builder setSender(@NonNull String setterArg) { + public @NonNull Builder setSender(@Nullable String setterArg) { this.sender = setterArg; return this; } private @Nullable String jid; - public @NonNull Builder setJid(@NonNull String setterArg) { + public @NonNull Builder setJid(@Nullable String setterArg) { this.jid = setterArg; return this; } @@ -664,6 +658,20 @@ public class Api { /** Generated class from Pigeon that represents data sent in messages. */ public static final class NotificationEvent { + /** The notification id. */ + private @NonNull Long id; + + public @NonNull Long getId() { + return id; + } + + public void setId(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"id\" is null."); + } + this.id = setterArg; + } + /** The JID the notification was for. */ private @NonNull String jid; @@ -723,6 +731,13 @@ public class Api { public static final class Builder { + private @Nullable Long id; + + public @NonNull Builder setId(@NonNull Long setterArg) { + this.id = setterArg; + return this; + } + private @Nullable String jid; public @NonNull Builder setJid(@NonNull String setterArg) { @@ -753,6 +768,7 @@ public class Api { public @NonNull NotificationEvent build() { NotificationEvent pigeonReturn = new NotificationEvent(); + pigeonReturn.setId(id); pigeonReturn.setJid(jid); pigeonReturn.setType(type); pigeonReturn.setPayload(payload); @@ -763,7 +779,8 @@ public class Api { @NonNull ArrayList toList() { - ArrayList toListResult = new ArrayList(4); + ArrayList toListResult = new ArrayList(5); + toListResult.add(id); toListResult.add(jid); toListResult.add(type == null ? null : type.index); toListResult.add(payload); @@ -773,13 +790,15 @@ public class Api { static @NonNull NotificationEvent fromList(@NonNull ArrayList list) { NotificationEvent pigeonResult = new NotificationEvent(); - Object jid = list.get(0); + Object id = list.get(0); + pigeonResult.setId((id == null) ? null : ((id instanceof Integer) ? (Integer) id : (Long) id)); + Object jid = list.get(1); pigeonResult.setJid((String) jid); - Object type = list.get(1); + Object type = list.get(2); pigeonResult.setType(type == null ? null : NotificationEventType.values()[(int) type]); - Object payload = list.get(2); + Object payload = list.get(3); pigeonResult.setPayload((String) payload); - Object extra = list.get(3); + Object extra = list.get(4); pigeonResult.setExtra((Map) extra); return pigeonResult; } 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 f608e16..675af52 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 @@ -58,6 +58,7 @@ class NotificationReceiver : BroadcastReceiver() { private fun handleMarkAsRead(context: Context, intent: Intent) { MoxplatformAndroidPlugin.notificationSink?.success( NotificationEvent().apply { + id = intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1) jid = intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!! type = Api.NotificationEventType.MARK_AS_READ payload = null @@ -74,6 +75,7 @@ class NotificationReceiver : BroadcastReceiver() { val replyPayload = remoteInput.getCharSequence(REPLY_TEXT_KEY) MoxplatformAndroidPlugin.notificationSink?.success( NotificationEvent().apply { + id = intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1) jid = intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!! type = Api.NotificationEventType.REPLY payload = replyPayload.toString() @@ -165,6 +167,7 @@ class NotificationReceiver : BroadcastReceiver() { private fun handleTap(context: Context, intent: Intent) { MoxplatformAndroidPlugin.notificationSink?.success( NotificationEvent().apply { + id = intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1) jid = intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!! type = Api.NotificationEventType.OPEN payload = null diff --git a/packages/moxplatform_platform_interface/lib/src/api.g.dart b/packages/moxplatform_platform_interface/lib/src/api.g.dart index 67246d7..2055b38 100644 --- a/packages/moxplatform_platform_interface/lib/src/api.g.dart +++ b/packages/moxplatform_platform_interface/lib/src/api.g.dart @@ -55,18 +55,18 @@ class NotificationMessageContent { class NotificationMessage { NotificationMessage({ - required this.sender, - required this.jid, + this.sender, + this.jid, required this.content, required this.timestamp, this.avatarPath, }); /// The sender of the message. - String sender; + String? sender; /// The jid of the sender. - String jid; + String? jid; /// The body of the message. NotificationMessageContent content; @@ -90,8 +90,8 @@ class NotificationMessage { static NotificationMessage decode(Object result) { result as List; return NotificationMessage( - sender: result[0]! as String, - jid: result[1]! as String, + sender: result[0] as String?, + jid: result[1] as String?, content: NotificationMessageContent.decode(result[2]! as List), timestamp: result[3]! as int, avatarPath: result[4] as String?, @@ -205,12 +205,16 @@ class RegularNotification { class NotificationEvent { NotificationEvent({ + required this.id, required this.jid, required this.type, this.payload, this.extra, }); + /// The notification id. + int id; + /// The JID the notification was for. String jid; @@ -227,6 +231,7 @@ class NotificationEvent { Object encode() { return [ + id, jid, type.index, payload, @@ -237,10 +242,11 @@ class NotificationEvent { 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?, - extra: (result[3] as Map?)?.cast(), + id: result[0]! as int, + jid: result[1]! as String, + type: NotificationEventType.values[result[2]! as int], + payload: result[3] as String?, + extra: (result[4] as Map?)?.cast(), ); } } diff --git a/pigeons/api.dart b/pigeons/api.dart index f68f400..7e1bfe9 100644 --- a/pigeons/api.dart +++ b/pigeons/api.dart @@ -38,10 +38,10 @@ class NotificationMessage { ); /// The sender of the message. - final String sender; + final String? sender; /// The jid of the sender. - final String jid; + final String? jid; /// The body of the message. final NotificationMessageContent content; @@ -111,12 +111,16 @@ enum NotificationEventType { class NotificationEvent { const NotificationEvent( + this.id, this.jid, this.type, this.payload, this.extra, ); + /// The notification id. + final int id; + /// The JID the notification was for. final String jid; From 79938aa177927f14ff5e2213bc43363e8e0fc064 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sun, 30 Jul 2023 22:28:16 +0200 Subject: [PATCH 15/23] fix: Fix self-replies after receiving another message If the order of events is - 1 or more messages received - 1 reply - 1 message received, then the self-reply will be grouped together with the message above it. --- .../java/me/polynom/moxplatform_android/Notifications.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 083575f..a33d50e 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 @@ -162,7 +162,12 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif val message = notification.messages[i] // Build the sender - val sender = Person.Builder().apply { + // NOTE: Note that we set it to null if message.sender == null because otherwise this results in + // a bogus Person object which messes with the "self-message" display as Android expects + // null in that case. + val sender = if (message.sender == null) + null + else Person.Builder().apply { setName(message.sender) setKey(message.jid) From 6896b928e8b94d4300d55f1c8f8687aa21e57029 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sun, 30 Jul 2023 22:40:02 +0200 Subject: [PATCH 16/23] feat: Store the avatar path also in the shared preferences --- .../polynom/moxplatform_android/Constants.kt | 1 + .../MoxplatformAndroidPlugin.java | 2 +- .../NotificationReceiver.kt | 5 ++-- .../moxplatform_android/Notifications.kt | 26 ++++++++++++++++--- 4 files changed, 28 insertions(+), 6 deletions(-) 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 3900268..edd0eef 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 @@ -32,6 +32,7 @@ const val SHARED_PREFERENCES_KEY = "me.polynom.moxplatform_android" const val SHARED_PREFERENCES_YOU_KEY = "you" const val SHARED_PREFERENCES_MARK_AS_READ_KEY = "mark_as_read" const val SHARED_PREFERENCES_REPLY_KEY = "reply" +const val SHARED_PREFERENCES_AVATAR_KEY = "avatar_path" // TODO: Maybe try again to rewrite the entire plugin in Kotlin //const val METHOD_CHANNEL_KEY = "me.polynom.moxplatform_android" 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 03833ed..85eb878 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 @@ -285,7 +285,7 @@ import kotlin.jvm.functions.Function1; @Override public void setNotificationSelfAvatar(@NonNull String path) { - NotificationDataManager.INSTANCE.setAvatarPath(path); + NotificationDataManager.INSTANCE.setAvatarPath(context, path); } @Override 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 675af52..fe7ea96 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 @@ -103,10 +103,11 @@ class NotificationReceiver : BroadcastReceiver() { setName(NotificationDataManager.getYou(context)) // Set an avatar, if we have one - if (NotificationDataManager.avatarPath != null) { + val avatarPath = NotificationDataManager.getAvatarPath(context) + if (avatarPath != null) { setIcon( Icon.createWithAdaptiveBitmap( - BitmapFactory.decodeFile(NotificationDataManager.avatarPath) + BitmapFactory.decodeFile(avatarPath) ) ) } 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 a33d50e..3c45912 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 @@ -24,7 +24,9 @@ object NotificationDataManager { private var you: String? = null private var markAsRead: String? = null private var reply: String? = null - var avatarPath: String? = null + + private var fetchedAvatarPath = false + private var avatarPath: String? = null private fun getString(context: Context, key: String, fallback: String): String { return context.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)!!.getString(key, fallback)!! @@ -66,6 +68,23 @@ object NotificationDataManager { setString(context, SHARED_PREFERENCES_REPLY_KEY, value) reply = value } + + fun getAvatarPath(context: Context): String? { + if (avatarPath == null && !fetchedAvatarPath) { + val path = getString(context, SHARED_PREFERENCES_AVATAR_KEY, "") + if (path.isNotEmpty()) { + avatarPath = path + } + } + + return avatarPath + } + + fun setAvatarPath(context: Context, value: String) { + setString(context, SHARED_PREFERENCES_AVATAR_KEY, value) + fetchedAvatarPath = true + avatarPath = value + } } /// Show a messaging style notification described by @notification. @@ -144,10 +163,11 @@ fun showMessagingNotification(context: Context, notification: Api.MessagingNotif setName(NotificationDataManager.getYou(context)) // Set an avatar, if we have one - if (NotificationDataManager.avatarPath != null) { + val avatarPath = NotificationDataManager.getAvatarPath(context) + if (avatarPath != null) { setIcon( IconCompat.createWithAdaptiveBitmap( - BitmapFactory.decodeFile(NotificationDataManager.avatarPath), + BitmapFactory.decodeFile(avatarPath), ), ) } From 271428219a576cfea4929e7d1ece1ad9e9c39810 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Thu, 3 Aug 2023 20:55:05 +0200 Subject: [PATCH 17/23] feat: Adjust to Moxxy changes --- packages/moxplatform_android/android/build.gradle | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/moxplatform_android/android/build.gradle b/packages/moxplatform_android/android/build.gradle index 0f01fe5..64bb097 100644 --- a/packages/moxplatform_android/android/build.gradle +++ b/packages/moxplatform_android/android/build.gradle @@ -27,16 +27,17 @@ apply plugin: 'com.android.library' apply plugin: 'org.jetbrains.kotlin.android' android { - compileSdkVersion 31 + compileSdkVersion 33 compileOptions { - sourceCompatibility JavaVersion.VERSION_14 - targetCompatibility JavaVersion.VERSION_14 + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } defaultConfig { // What Moxxy currently uses minSdkVersion 26 + targetSdkVersion 33 } } From 61de3cd565aaba0b1122d72468a9f6784327eac0 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Thu, 3 Aug 2023 21:19:11 +0200 Subject: [PATCH 18/23] feat: Move the crypto APIs to pigeon --- .../me/polynom/moxplatform_android/Api.java | 227 +++++++++++++++++- .../me/polynom/moxplatform_android/Crypto.kt | 182 +++++++------- .../MoxplatformAndroidPlugin.java | 80 +++--- .../lib/src/crypto_android.dart | 40 +-- .../lib/src/api.g.dart | 129 +++++++++- .../lib/src/crypto.dart | 22 +- .../lib/src/crypto_stub.dart | 3 +- pigeons/api.dart | 22 ++ 8 files changed, 496 insertions(+), 209 deletions(-) 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 20bf705..32d0d1e 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 @@ -81,6 +81,18 @@ public class Api { } } + public enum CipherAlgorithm { + AES128GCM_NO_PADDING(0), + AES256GCM_NO_PADDING(1), + AES256CBC_PKCS7(2); + + final int index; + + private CipherAlgorithm(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. */ @@ -904,6 +916,86 @@ public class Api { } } + /** Generated class from Pigeon that represents data sent in messages. */ + public static final class CryptographyResult { + private @NonNull byte[] plaintextHash; + + public @NonNull byte[] getPlaintextHash() { + return plaintextHash; + } + + public void setPlaintextHash(@NonNull byte[] setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"plaintextHash\" is null."); + } + this.plaintextHash = setterArg; + } + + private @NonNull byte[] ciphertextHash; + + public @NonNull byte[] getCiphertextHash() { + return ciphertextHash; + } + + public void setCiphertextHash(@NonNull byte[] setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"ciphertextHash\" is null."); + } + this.ciphertextHash = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + CryptographyResult() {} + + public static final class Builder { + + private @Nullable byte[] plaintextHash; + + public @NonNull Builder setPlaintextHash(@NonNull byte[] setterArg) { + this.plaintextHash = setterArg; + return this; + } + + private @Nullable byte[] ciphertextHash; + + public @NonNull Builder setCiphertextHash(@NonNull byte[] setterArg) { + this.ciphertextHash = setterArg; + return this; + } + + public @NonNull CryptographyResult build() { + CryptographyResult pigeonReturn = new CryptographyResult(); + pigeonReturn.setPlaintextHash(plaintextHash); + pigeonReturn.setCiphertextHash(ciphertextHash); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add(plaintextHash); + toListResult.add(ciphertextHash); + return toListResult; + } + + static @NonNull CryptographyResult fromList(@NonNull ArrayList list) { + CryptographyResult pigeonResult = new CryptographyResult(); + Object plaintextHash = list.get(0); + pigeonResult.setPlaintextHash((byte[]) plaintextHash); + Object ciphertextHash = list.get(1); + pigeonResult.setCiphertextHash((byte[]) ciphertextHash); + return pigeonResult; + } + } + + public interface Result { + @SuppressWarnings("UnknownNullness") + void success(T result); + + void error(@NonNull Throwable error); + } + private static class MoxplatformApiCodec extends StandardMessageCodec { public static final MoxplatformApiCodec INSTANCE = new MoxplatformApiCodec(); @@ -913,16 +1005,18 @@ public class Api { protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { case (byte) 128: - return MessagingNotification.fromList((ArrayList) readValue(buffer)); + return CryptographyResult.fromList((ArrayList) readValue(buffer)); case (byte) 129: - return NotificationEvent.fromList((ArrayList) readValue(buffer)); + return MessagingNotification.fromList((ArrayList) readValue(buffer)); case (byte) 130: - return NotificationI18nData.fromList((ArrayList) readValue(buffer)); + return NotificationEvent.fromList((ArrayList) readValue(buffer)); case (byte) 131: - return NotificationMessage.fromList((ArrayList) readValue(buffer)); + return NotificationI18nData.fromList((ArrayList) readValue(buffer)); case (byte) 132: - return NotificationMessageContent.fromList((ArrayList) readValue(buffer)); + return NotificationMessage.fromList((ArrayList) readValue(buffer)); case (byte) 133: + return NotificationMessageContent.fromList((ArrayList) readValue(buffer)); + case (byte) 134: return RegularNotification.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); @@ -931,23 +1025,26 @@ public class Api { @Override protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { - if (value instanceof MessagingNotification) { + if (value instanceof CryptographyResult) { stream.write(128); + writeValue(stream, ((CryptographyResult) value).toList()); + } else if (value instanceof MessagingNotification) { + stream.write(129); writeValue(stream, ((MessagingNotification) value).toList()); } else if (value instanceof NotificationEvent) { - stream.write(129); + stream.write(130); writeValue(stream, ((NotificationEvent) value).toList()); } else if (value instanceof NotificationI18nData) { - stream.write(130); + stream.write(131); writeValue(stream, ((NotificationI18nData) value).toList()); } else if (value instanceof NotificationMessage) { - stream.write(131); + stream.write(132); writeValue(stream, ((NotificationMessage) value).toList()); } else if (value instanceof NotificationMessageContent) { - stream.write(132); + stream.write(133); writeValue(stream, ((NotificationMessageContent) value).toList()); } else if (value instanceof RegularNotification) { - stream.write(133); + stream.write(134); writeValue(stream, ((RegularNotification) value).toList()); } else { super.writeValue(stream, value); @@ -957,7 +1054,7 @@ public class Api { /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface MoxplatformApi { - + /** Notification APIs */ void createNotificationChannel(@NonNull String title, @NonNull String description, @NonNull String id, @NonNull Boolean urgent); void showMessagingNotification(@NonNull MessagingNotification notification); @@ -969,13 +1066,19 @@ public class Api { void setNotificationSelfAvatar(@NonNull String path); void setNotificationI18n(@NonNull NotificationI18nData data); - + /** Platform APIs */ @NonNull String getPersistentDataPath(); @NonNull String getCacheDataPath(); + /** Cryptography APIs */ + void encryptFile(@NonNull String sourcePath, @NonNull String destPath, @NonNull byte[] key, @NonNull byte[] iv, @NonNull CipherAlgorithm algorithm, @NonNull String hashSpec, @NonNull Result result); + void decryptFile(@NonNull String sourcePath, @NonNull String destPath, @NonNull byte[] key, @NonNull byte[] iv, @NonNull CipherAlgorithm algorithm, @NonNull String hashSpec, @NonNull Result result); + + void hashFile(@NonNull String sourcePath, @NonNull String hashSpec, @NonNull Result result); + /** Stubs */ void eventStub(@NonNull NotificationEvent event); /** The codec used by MoxplatformApi. */ @@ -1175,6 +1278,104 @@ public class Api { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.encryptFile", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + String sourcePathArg = (String) args.get(0); + String destPathArg = (String) args.get(1); + byte[] keyArg = (byte[]) args.get(2); + byte[] ivArg = (byte[]) args.get(3); + CipherAlgorithm algorithmArg = args.get(4) == null ? null : CipherAlgorithm.values()[(int) args.get(4)]; + String hashSpecArg = (String) args.get(5); + Result resultCallback = + new Result() { + public void success(CryptographyResult result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.encryptFile(sourcePathArg, destPathArg, keyArg, ivArg, algorithmArg, hashSpecArg, resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.decryptFile", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + String sourcePathArg = (String) args.get(0); + String destPathArg = (String) args.get(1); + byte[] keyArg = (byte[]) args.get(2); + byte[] ivArg = (byte[]) args.get(3); + CipherAlgorithm algorithmArg = args.get(4) == null ? null : CipherAlgorithm.values()[(int) args.get(4)]; + String hashSpecArg = (String) args.get(5); + Result resultCallback = + new Result() { + public void success(CryptographyResult result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.decryptFile(sourcePathArg, destPathArg, keyArg, ivArg, algorithmArg, hashSpecArg, resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.hashFile", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + String sourcePathArg = (String) args.get(0); + String hashSpecArg = (String) args.get(1); + Result resultCallback = + new Result() { + public void success(byte[] result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.hashFile(sourcePathArg, hashSpecArg, resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } { BasicMessageChannel channel = new BasicMessageChannel<>( diff --git a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Crypto.kt b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Crypto.kt index 52f011c..63bca9d 100644 --- a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Crypto.kt +++ b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Crypto.kt @@ -1,6 +1,8 @@ package me.polynom.moxplatform_android import android.util.Log +import me.polynom.moxplatform_android.Api.CipherAlgorithm +import me.polynom.moxplatform_android.Api.CryptographyResult import java.io.FileInputStream import java.io.FileOutputStream @@ -10,6 +12,7 @@ import javax.crypto.Cipher import javax.crypto.CipherOutputStream import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec +import kotlin.concurrent.thread // A FileOutputStream that continuously hashes whatever it writes to the file. private class HashedFileOutputStream(name: String, hashAlgorithm: String) : FileOutputStream(name) { @@ -30,115 +33,126 @@ private class HashedFileOutputStream(name: String, hashAlgorithm: String) : File } } -fun getCipherSpecFromInteger(algorithmType: Int): String { - return when (algorithmType) { - 0 -> "AES_128/GCM/NoPadding" - 1 -> "AES_256/GCM/NoPadding" - 2 -> "AES_256/CBC/PKCS7PADDING" - else -> "" +fun getCipherSpecFromInteger(algorithm: CipherAlgorithm): String { + return when (algorithm) { + CipherAlgorithm.AES128GCM_NO_PADDING -> "AES_128/GCM/NoPadding" + CipherAlgorithm.AES256GCM_NO_PADDING -> "AES_256/GCM/NoPadding" + CipherAlgorithm.AES256CBC_PKCS7 -> "AES_256/CBC/PKCS7PADDING" } } // Compute the hash, specified by @algorithm, of the file at path @srcFile. If an exception // occurs, returns null. If everything went well, returns the raw hash of @srcFile. -fun hashFile(srcFile: String, algorithm: String): ByteArray? { - val buffer = ByteArray(BUFFER_SIZE) - try { - val digest = MessageDigest.getInstance(algorithm) - val fInputStream = FileInputStream(srcFile) - var length: Int +fun hashFile(srcFile: String, algorithm: String, result: Api.Result) { + thread(start = true) { + val buffer = ByteArray(BUFFER_SIZE) + try { + val digest = MessageDigest.getInstance(algorithm) + val fInputStream = FileInputStream(srcFile) + var length: Int - while (true) { - length = fInputStream.read() - if (length <= 0) break + while (true) { + length = fInputStream.read() + if (length <= 0) break - // Only update the digest if we read more than 0 bytes - digest.update(buffer, 0, length) + // Only update the digest if we read more than 0 bytes + digest.update(buffer, 0, length) + } + + fInputStream.close() + + result.success(digest.digest()) + } catch (e: Exception) { + Log.e(TAG, "[hashFile]: " + e.stackTraceToString()) + result.success(null) } - - fInputStream.close() - - return digest.digest() - } catch (e: Exception) { - Log.e(TAG, "[hashFile]: " + e.stackTraceToString()) - return null } } // Encrypt the plaintext file at @src to @dest using the secret key @key and the IV @iv. The algorithm is chosen using @cipherAlgorithm. The file is additionally // hashed before and after encryption using the hash algorithm specified by @hashAlgorithm. -fun encryptAndHash(src: String, dest: String, key: ByteArray, iv: ByteArray, cipherAlgorithm: String, hashAlgorithm: String): HashMap? { - val buffer = ByteArray(BUFFER_SIZE) - val secretKey = SecretKeySpec(key, cipherAlgorithm) - try { - val digest = MessageDigest.getInstance(hashAlgorithm) - val cipher = Cipher.getInstance(cipherAlgorithm) - cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iv)) +fun encryptAndHash(src: String, dest: String, key: ByteArray, iv: ByteArray, cipherAlgorithm: CipherAlgorithm, hashAlgorithm: String, result: Api.Result) { + thread(start = true) { + val cipherSpec = getCipherSpecFromInteger(cipherAlgorithm) + val buffer = ByteArray(BUFFER_SIZE) + val secretKey = SecretKeySpec(key, cipherSpec) + try { + val digest = MessageDigest.getInstance(hashAlgorithm) + val cipher = Cipher.getInstance(cipherSpec) + cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iv)) - val fileInputStream = FileInputStream(src) - val fileOutputStream = HashedFileOutputStream(dest, hashAlgorithm) - val cipherOutputStream = CipherOutputStream(fileOutputStream, cipher) + val fileInputStream = FileInputStream(src) + val fileOutputStream = HashedFileOutputStream(dest, hashAlgorithm) + val cipherOutputStream = CipherOutputStream(fileOutputStream, cipher) - var length: Int - while (true) { - length = fileInputStream.read(buffer) - if (length <= 0) break + var length: Int + while (true) { + length = fileInputStream.read(buffer) + if (length <= 0) break - digest.update(buffer, 0, length) - cipherOutputStream.write(buffer, 0, length) + digest.update(buffer, 0, length) + cipherOutputStream.write(buffer, 0, length) + } + + // Flush and close + cipherOutputStream.flush() + cipherOutputStream.close() + fileInputStream.close() + + result.success( + CryptographyResult().apply { + plaintextHash = digest.digest() + ciphertextHash = fileOutputStream.digest() + } + ) + } catch (e: Exception) { + Log.e(TAG, "[encryptAndHash]: " + e.stackTraceToString()) + result.success(null) } - - // Flush and close - cipherOutputStream.flush() - cipherOutputStream.close() - fileInputStream.close() - - return hashMapOf( - "plaintextHash" to digest.digest(), - "ciphertextHash" to fileOutputStream.digest(), - ) - } catch (e: Exception) { - Log.e(TAG, "[encryptAndHash]: " + e.stackTraceToString()) - return null } } // Decrypt the ciphertext file at @src to @dest using the secret key @key and the IV @iv. The algorithm is chosen using @cipherAlgorithm. The file is additionally // hashed before and after decryption using the hash algorithm specified by @hashAlgorithm. -fun decryptAndHash(src: String, dest: String, key: ByteArray, iv: ByteArray, cipherAlgorithm: String, hashAlgorithm: String): HashMap? { - // Shamelessly stolen from https://github.com/hugo-pcl/native-crypto-flutter/pull/3 - val buffer = ByteArray(BUFFER_SIZE) - val secretKey = SecretKeySpec(key, cipherAlgorithm) - try { - val digest = MessageDigest.getInstance(hashAlgorithm) - val cipher = Cipher.getInstance(cipherAlgorithm) - cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iv)) +fun decryptAndHash(src: String, dest: String, key: ByteArray, iv: ByteArray, cipherAlgorithm: CipherAlgorithm, hashAlgorithm: String, result: Api.Result) { + thread(start = true) { + val cipherSpec = getCipherSpecFromInteger(cipherAlgorithm) + // Shamelessly stolen from https://github.com/hugo-pcl/native-crypto-flutter/pull/3 + val buffer = ByteArray(BUFFER_SIZE) + val secretKey = SecretKeySpec(key, cipherSpec) + try { + val digest = MessageDigest.getInstance(hashAlgorithm) + val cipher = Cipher.getInstance(cipherSpec) + cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iv)) - val fileInputStream = FileInputStream(src) - val fileOutputStream = HashedFileOutputStream(dest, hashAlgorithm) - val cipherOutputStream = CipherOutputStream(fileOutputStream, cipher) + val fileInputStream = FileInputStream(src) + val fileOutputStream = HashedFileOutputStream(dest, hashAlgorithm) + val cipherOutputStream = CipherOutputStream(fileOutputStream, cipher) - // Read, decrypt, and hash until we read 0 bytes - var length: Int - while (true) { - length = fileInputStream.read(buffer) - if (length <= 0) break + // Read, decrypt, and hash until we read 0 bytes + var length: Int + while (true) { + length = fileInputStream.read(buffer) + if (length <= 0) break - digest.update(buffer, 0, length) - cipherOutputStream.write(buffer, 0, length) + digest.update(buffer, 0, length) + cipherOutputStream.write(buffer, 0, length) + } + + // Flush + cipherOutputStream.flush() + cipherOutputStream.close() + fileInputStream.close() + + result.success( + CryptographyResult().apply { + plaintextHash = digest.digest() + ciphertextHash = fileOutputStream.digest() + } + ) + } catch (e: Exception) { + Log.e(TAG, "[hashAndDecrypt]: " + e.stackTraceToString()) + result.success(null) } - - // Flush - cipherOutputStream.flush() - cipherOutputStream.close() - fileInputStream.close() - - return hashMapOf( - "plaintextHash" to digest.digest(), - "ciphertextHash" to fileOutputStream.digest(), - ) - } catch (e: Exception) { - Log.e(TAG, "[hashAndDecrypt]: " + e.stackTraceToString()) - return null } } \ No newline at end of file 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 85eb878..c9d3ce6 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 @@ -40,7 +40,7 @@ import io.flutter.plugin.common.JSONMethodCodec; import kotlin.Unit; import kotlin.jvm.functions.Function1; - public class MoxplatformAndroidPlugin extends BroadcastReceiver implements FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler, 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"; @@ -166,53 +166,6 @@ import kotlin.jvm.functions.Function1; } result.success(true); break; - case "encryptFile": - Thread encryptionThread = new Thread(new Runnable() { - @Override - public void run() { - ArrayList args = (ArrayList) call.arguments; - String src = (String) args.get(0); - String dest = (String) args.get(1); - byte[] key = (byte[]) args.get(2); - byte[] iv = (byte[]) args.get(3); - int algorithm = (int) args.get(4); - String hashSpec = (String) args.get(5); - - result.success(encryptAndHash(src, dest, key, iv, getCipherSpecFromInteger(algorithm), hashSpec)); - } - }); - encryptionThread.start(); - break; - case "decryptFile": - Thread decryptionThread = new Thread(new Runnable() { - @Override - public void run() { - ArrayList args = (ArrayList) call.arguments; - String src = (String) args.get(0); - String dest = (String) args.get(1); - byte[] key = (byte[]) args.get(2); - byte[] iv = (byte[]) args.get(3); - int algorithm = (int) args.get(4); - String hashSpec = (String) args.get(5); - - result.success(decryptAndHash(src, dest, key, iv, getCipherSpecFromInteger(algorithm), hashSpec)); - } - }); - decryptionThread.start(); - break; - case "hashFile": - Thread hashingThread = new Thread(new Runnable() { - @Override - public void run() { - ArrayList args = (ArrayList) call.arguments; - String src = (String) args.get(0); - String hashSpec = (String) args.get(1); - - result.success(hashFile(src, hashSpec)); - } - }); - hashingThread.start(); - break; case "recordSentMessage": ArrayList rargs = (ArrayList) call.arguments; recordSentMessage(context, (String) rargs.get(0), (String) rargs.get(1), (String) rargs.get(2), (int) rargs.get(3)); @@ -308,6 +261,37 @@ import kotlin.jvm.functions.Function1; return context.getCacheDir().getPath(); } + @Override + public void encryptFile(@NonNull String sourcePath, @NonNull String destPath, @NonNull byte[] key, @NonNull byte[] iv, @NonNull CipherAlgorithm algorithm, @NonNull String hashSpec, @NonNull Api.Result result) { + CryptoKt.encryptAndHash( + sourcePath, + destPath, + key, + iv, + algorithm, + hashSpec, + result + ); + } + + @Override + public void decryptFile(@NonNull String sourcePath, @NonNull String destPath, @NonNull byte[] key, @NonNull byte[] iv, @NonNull CipherAlgorithm algorithm, @NonNull String hashSpec, @NonNull Api.Result result) { + CryptoKt.decryptAndHash( + sourcePath, + destPath, + key, + iv, + algorithm, + hashSpec, + result + ); + } + + @Override + public void hashFile(@NonNull String sourcePath, @NonNull String hashSpec, @NonNull Api.Result result) { + CryptoKt.hashFile(sourcePath, hashSpec, result); + } + @Override public void eventStub(@NonNull NotificationEvent event) { // Stub to trick pigeon into diff --git a/packages/moxplatform_android/lib/src/crypto_android.dart b/packages/moxplatform_android/lib/src/crypto_android.dart index e203b36..e1b71ba 100644 --- a/packages/moxplatform_android/lib/src/crypto_android.dart +++ b/packages/moxplatform_android/lib/src/crypto_android.dart @@ -2,7 +2,7 @@ import 'package:flutter/services.dart'; import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart'; class AndroidCryptographyImplementation extends CryptographyImplementation { - final _methodChannel = const MethodChannel('me.polynom.moxplatform_android'); + final MoxplatformApi _api = MoxplatformApi(); @override Future encryptFile( @@ -13,22 +13,13 @@ class AndroidCryptographyImplementation extends CryptographyImplementation { CipherAlgorithm algorithm, String hashSpec, ) async { - final dynamic resultRaw = - await _methodChannel.invokeMethod('encryptFile', [ + return _api.encryptFile( sourcePath, destPath, key, iv, - algorithm.value, + algorithm, hashSpec, - ]); - if (resultRaw == null) return null; - - // ignore: argument_type_not_assignable - final result = Map.from(resultRaw); - return CryptographyResult( - result['plaintextHash']! as Uint8List, - result['ciphertextHash']! as Uint8List, ); } @@ -41,35 +32,18 @@ class AndroidCryptographyImplementation extends CryptographyImplementation { CipherAlgorithm algorithm, String hashSpec, ) async { - final dynamic resultRaw = - await _methodChannel.invokeMethod('decryptFile', [ + return _api.decryptFile( sourcePath, destPath, key, iv, - algorithm.value, + algorithm, hashSpec, - ]); - if (resultRaw == null) return null; - - // ignore: argument_type_not_assignable - final result = Map.from(resultRaw); - return CryptographyResult( - result['plaintextHash']! as Uint8List, - result['ciphertextHash']! as Uint8List, ); } @override - Future hashFile(String path, String hashSpec) async { - final dynamic resultsRaw = - await _methodChannel.invokeMethod('hashFile', [ - path, - hashSpec, - ]); - - if (resultsRaw == null) return null; - - return resultsRaw as Uint8List; + Future hashFile(String sourcePath, String hashSpec) async { + return _api.hashFile(sourcePath, hashSpec); } } diff --git a/packages/moxplatform_platform_interface/lib/src/api.g.dart b/packages/moxplatform_platform_interface/lib/src/api.g.dart index 2055b38..a2fe506 100644 --- a/packages/moxplatform_platform_interface/lib/src/api.g.dart +++ b/packages/moxplatform_platform_interface/lib/src/api.g.dart @@ -20,6 +20,12 @@ enum NotificationEventType { open, } +enum CipherAlgorithm { + aes128GcmNoPadding, + aes256GcmNoPadding, + aes256CbcPkcs7, +} + class NotificationMessageContent { NotificationMessageContent({ this.body, @@ -285,28 +291,57 @@ class NotificationI18nData { } } +class CryptographyResult { + CryptographyResult({ + required this.plaintextHash, + required this.ciphertextHash, + }); + + Uint8List plaintextHash; + + Uint8List ciphertextHash; + + Object encode() { + return [ + plaintextHash, + ciphertextHash, + ]; + } + + static CryptographyResult decode(Object result) { + result as List; + return CryptographyResult( + plaintextHash: result[0]! as Uint8List, + ciphertextHash: result[1]! as Uint8List, + ); + } +} + class _MoxplatformApiCodec extends StandardMessageCodec { const _MoxplatformApiCodec(); @override void writeValue(WriteBuffer buffer, Object? value) { - if (value is MessagingNotification) { + if (value is CryptographyResult) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else if (value is NotificationEvent) { + } else if (value is MessagingNotification) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is NotificationI18nData) { + } else if (value is NotificationEvent) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is NotificationMessage) { + } else if (value is NotificationI18nData) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is NotificationMessageContent) { + } else if (value is NotificationMessage) { buffer.putUint8(132); writeValue(buffer, value.encode()); - } else if (value is RegularNotification) { + } else if (value is NotificationMessageContent) { buffer.putUint8(133); writeValue(buffer, value.encode()); + } else if (value is RegularNotification) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -316,16 +351,18 @@ class _MoxplatformApiCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 128: - return MessagingNotification.decode(readValue(buffer)!); + return CryptographyResult.decode(readValue(buffer)!); case 129: - return NotificationEvent.decode(readValue(buffer)!); + return MessagingNotification.decode(readValue(buffer)!); case 130: - return NotificationI18nData.decode(readValue(buffer)!); + return NotificationEvent.decode(readValue(buffer)!); case 131: - return NotificationMessage.decode(readValue(buffer)!); + return NotificationI18nData.decode(readValue(buffer)!); case 132: - return NotificationMessageContent.decode(readValue(buffer)!); + return NotificationMessage.decode(readValue(buffer)!); case 133: + return NotificationMessageContent.decode(readValue(buffer)!); + case 134: return RegularNotification.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -343,6 +380,7 @@ class MoxplatformApi { static const MessageCodec codec = _MoxplatformApiCodec(); + /// Notification APIs Future createNotificationChannel(String arg_title, String arg_description, String arg_id, bool arg_urgent) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.createNotificationChannel', codec, @@ -475,6 +513,7 @@ class MoxplatformApi { } } + /// Platform APIs Future getPersistentDataPath() async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.getPersistentDataPath', codec, @@ -529,6 +568,74 @@ class MoxplatformApi { } } + /// Cryptography APIs + Future encryptFile(String arg_sourcePath, String arg_destPath, Uint8List arg_key, Uint8List arg_iv, CipherAlgorithm arg_algorithm, String arg_hashSpec) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.encryptFile', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_sourcePath, arg_destPath, arg_key, arg_iv, arg_algorithm.index, arg_hashSpec]) 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 (replyList[0] as CryptographyResult?); + } + } + + Future decryptFile(String arg_sourcePath, String arg_destPath, Uint8List arg_key, Uint8List arg_iv, CipherAlgorithm arg_algorithm, String arg_hashSpec) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.decryptFile', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_sourcePath, arg_destPath, arg_key, arg_iv, arg_algorithm.index, arg_hashSpec]) 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 (replyList[0] as CryptographyResult?); + } + } + + Future hashFile(String arg_sourcePath, String arg_hashSpec) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.hashFile', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_sourcePath, arg_hashSpec]) 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 (replyList[0] as Uint8List?); + } + } + + /// Stubs Future eventStub(NotificationEvent arg_event) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.eventStub', codec, diff --git a/packages/moxplatform_platform_interface/lib/src/crypto.dart b/packages/moxplatform_platform_interface/lib/src/crypto.dart index b61b086..b3f7121 100644 --- a/packages/moxplatform_platform_interface/lib/src/crypto.dart +++ b/packages/moxplatform_platform_interface/lib/src/crypto.dart @@ -1,21 +1,5 @@ import 'dart:typed_data'; - -enum CipherAlgorithm { - aes128GcmNoPadding(0), - aes256GcmNoPadding(1), - aes256CbcPkcs7(2); - - const CipherAlgorithm(this.value); - - /// The "id" of the algorithm choice. - final int value; -} - -class CryptographyResult { - const CryptographyResult(this.plaintextHash, this.ciphertextHash); - final Uint8List plaintextHash; - final Uint8List ciphertextHash; -} +import 'package:moxplatform_platform_interface/src/api.g.dart'; /// Wrapper around platform-native cryptography APIs abstract class CryptographyImplementation { @@ -47,9 +31,9 @@ abstract class CryptographyImplementation { String hashSpec, ); - /// Hashes the file at [path] using the Hash function with name [hashSpec]. + /// Hashes the file at [sourcePath] using the Hash function with name [hashSpec]. /// Note that this function runs off-thread as to not block the UI thread. /// /// Returns the hash of the file. - Future hashFile(String path, String hashSpec); + Future hashFile(String sourcePath, String hashSpec); } diff --git a/packages/moxplatform_platform_interface/lib/src/crypto_stub.dart b/packages/moxplatform_platform_interface/lib/src/crypto_stub.dart index b71ff68..b1517f4 100644 --- a/packages/moxplatform_platform_interface/lib/src/crypto_stub.dart +++ b/packages/moxplatform_platform_interface/lib/src/crypto_stub.dart @@ -1,4 +1,5 @@ import 'dart:typed_data'; +import 'package:moxplatform_platform_interface/src/api.g.dart'; import 'package:moxplatform_platform_interface/src/crypto.dart'; class StubCryptographyImplementation extends CryptographyImplementation { @@ -27,7 +28,7 @@ class StubCryptographyImplementation extends CryptographyImplementation { } @override - Future hashFile(String path, String hashSpec) async { + Future hashFile(String sourcePath, String hashSpec) async { return null; } } diff --git a/pigeons/api.dart b/pigeons/api.dart index 7e1bfe9..8dd2ca7 100644 --- a/pigeons/api.dart +++ b/pigeons/api.dart @@ -149,15 +149,37 @@ class NotificationI18nData { final String you; } +enum CipherAlgorithm { + aes128GcmNoPadding, + aes256GcmNoPadding, + aes256CbcPkcs7; +} + +class CryptographyResult { + const CryptographyResult(this.plaintextHash, this.ciphertextHash); + final Uint8List plaintextHash; + final Uint8List ciphertextHash; +} + @HostApi() abstract class MoxplatformApi { + /// Notification APIs void createNotificationChannel(String title, String description, String id, bool urgent); void showMessagingNotification(MessagingNotification notification); void showNotification(RegularNotification notification); void dismissNotification(int id); void setNotificationSelfAvatar(String path); void setNotificationI18n(NotificationI18nData data); + + /// Platform APIs String getPersistentDataPath(); String getCacheDataPath(); + + /// Cryptography APIs + @async CryptographyResult? encryptFile(String sourcePath, String destPath, Uint8List key, Uint8List iv, CipherAlgorithm algorithm, String hashSpec); + @async CryptographyResult? decryptFile(String sourcePath, String destPath, Uint8List key, Uint8List iv, CipherAlgorithm algorithm, String hashSpec); + @async Uint8List? hashFile(String sourcePath, String hashSpec); + + /// Stubs void eventStub(NotificationEvent event); } From b12e36da83dde609dccd2e24c341a271aa63be7e Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Thu, 3 Aug 2023 21:27:13 +0200 Subject: [PATCH 19/23] feat: Move recordSentMessage to pigeon --- .../me/polynom/moxplatform_android/Api.java | 41 +++++++++++++++++++ .../MoxplatformAndroidPlugin.java | 11 ++--- .../moxplatform_android/RecordSentMessage.kt | 11 +++-- .../lib/src/contacts_android.dart | 17 ++++---- .../lib/src/api.g.dart | 29 +++++++++++++ .../lib/src/contacts.dart | 12 +----- .../lib/src/contacts_stub.dart | 1 + pigeons/api.dart | 10 +++++ 8 files changed, 102 insertions(+), 30 deletions(-) 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 32d0d1e..4f1cbc4 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 @@ -93,6 +93,18 @@ public class Api { } } + public enum FallbackIconType { + NONE(0), + PERSON(1), + NOTES(2); + + final int index; + + private FallbackIconType(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. */ @@ -1072,6 +1084,8 @@ public class Api { @NonNull String getCacheDataPath(); + /** Contacts APIs */ + void recordSentMessage(@NonNull String name, @NonNull String jid, @Nullable String avatarPath, @NonNull FallbackIconType fallbackIcon); /** Cryptography APIs */ void encryptFile(@NonNull String sourcePath, @NonNull String destPath, @NonNull byte[] key, @NonNull byte[] iv, @NonNull CipherAlgorithm algorithm, @NonNull String hashSpec, @NonNull Result result); @@ -1268,6 +1282,33 @@ 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.recordSentMessage", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + String nameArg = (String) args.get(0); + String jidArg = (String) args.get(1); + String avatarPathArg = (String) args.get(2); + FallbackIconType fallbackIconArg = args.get(3) == null ? null : FallbackIconType.values()[(int) args.get(3)]; + try { + api.recordSentMessage(nameArg, jidArg, avatarPathArg, fallbackIconArg); + 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/MoxplatformAndroidPlugin.java b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/MoxplatformAndroidPlugin.java index c9d3ce6..664b7ea 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 @@ -18,6 +18,7 @@ import android.content.SharedPreferences; import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.app.NotificationManagerCompat; import androidx.core.content.ContextCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; @@ -166,11 +167,6 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt } result.success(true); break; - case "recordSentMessage": - ArrayList rargs = (ArrayList) call.arguments; - recordSentMessage(context, (String) rargs.get(0), (String) rargs.get(1), (String) rargs.get(2), (int) rargs.get(3)); - result.success(true); - break; default: result.notImplemented(); break; @@ -261,6 +257,11 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt return context.getCacheDir().getPath(); } + @Override + public void recordSentMessage(@NonNull String name, @NonNull String jid, @Nullable String avatarPath, @NonNull FallbackIconType fallbackIcon) { + systemRecordSentMessage(context, name, jid, avatarPath, fallbackIcon); + } + @Override public void encryptFile(@NonNull String sourcePath, @NonNull String destPath, @NonNull byte[] key, @NonNull byte[] iv, @NonNull CipherAlgorithm algorithm, @NonNull String hashSpec, @NonNull Api.Result result) { CryptoKt.encryptAndHash( diff --git a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/RecordSentMessage.kt b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/RecordSentMessage.kt index dce2063..bdaaede 100644 --- a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/RecordSentMessage.kt +++ b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/RecordSentMessage.kt @@ -7,12 +7,15 @@ import androidx.core.app.Person import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.drawable.IconCompat +import me.polynom.moxplatform_android.Api.FallbackIconType /* * Uses Android's direct share API to create dynamic share targets that are compatible * with share_handler's media handling. + * NOTE: The "system" prefix is to prevent confusion between pigeon's abstract recordSentMessage + * method and this one. * */ -fun recordSentMessage(context: Context, name: String, jid: String, avatarPath: String?, fallbackIconType: Int) { +fun systemRecordSentMessage(context: Context, name: String, jid: String, avatarPath: String?, fallbackIcon: FallbackIconType) { val pkgName = context.packageName val intent = Intent(context, Class.forName("$pkgName.MainActivity")).apply { action = Intent.ACTION_SEND @@ -43,9 +46,9 @@ fun recordSentMessage(context: Context, name: String, jid: String, avatarPath: S shortcutBuilder.setIcon(icon) personBuilder.setIcon(icon) } else { - val resourceId = when(fallbackIconType) { - 0 -> R.mipmap.person - 1 -> R.mipmap.notes + val resourceId = when(fallbackIcon) { + FallbackIconType.PERSON -> R.mipmap.person + FallbackIconType.NOTES -> R.mipmap.notes // "Fallthrough" else -> R.mipmap.person } diff --git a/packages/moxplatform_android/lib/src/contacts_android.dart b/packages/moxplatform_android/lib/src/contacts_android.dart index 6671897..c21ea4a 100644 --- a/packages/moxplatform_android/lib/src/contacts_android.dart +++ b/packages/moxplatform_android/lib/src/contacts_android.dart @@ -1,8 +1,8 @@ -import 'package:flutter/services.dart'; import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart'; +import 'package:moxplatform_platform_interface/src/api.g.dart'; class AndroidContactsImplementation extends ContactsImplementation { - final _methodChannel = const MethodChannel('me.polynom.moxplatform_android'); + final MoxplatformApi _api = MoxplatformApi(); @override Future recordSentMessage( @@ -19,14 +19,11 @@ class AndroidContactsImplementation extends ContactsImplementation { ); } - await _methodChannel.invokeMethod( - 'recordSentMessage', - [ - name, - jid, - avatarPath, - fallbackIcon.id, - ], + return _api.recordSentMessage( + name, + jid, + avatarPath, + fallbackIcon, ); } } diff --git a/packages/moxplatform_platform_interface/lib/src/api.g.dart b/packages/moxplatform_platform_interface/lib/src/api.g.dart index a2fe506..0725d2e 100644 --- a/packages/moxplatform_platform_interface/lib/src/api.g.dart +++ b/packages/moxplatform_platform_interface/lib/src/api.g.dart @@ -26,6 +26,12 @@ enum CipherAlgorithm { aes256CbcPkcs7, } +enum FallbackIconType { + none, + person, + notes, +} + class NotificationMessageContent { NotificationMessageContent({ this.body, @@ -568,6 +574,29 @@ class MoxplatformApi { } } + /// Contacts APIs + Future recordSentMessage(String arg_name, String arg_jid, String? arg_avatarPath, FallbackIconType arg_fallbackIcon) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.recordSentMessage', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_name, arg_jid, arg_avatarPath, arg_fallbackIcon.index]) 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; + } + } + /// Cryptography APIs Future encryptFile(String arg_sourcePath, String arg_destPath, Uint8List arg_key, Uint8List arg_iv, CipherAlgorithm arg_algorithm, String arg_hashSpec) async { final BasicMessageChannel channel = BasicMessageChannel( diff --git a/packages/moxplatform_platform_interface/lib/src/contacts.dart b/packages/moxplatform_platform_interface/lib/src/contacts.dart index 3ee6a22..ab6ad01 100644 --- a/packages/moxplatform_platform_interface/lib/src/contacts.dart +++ b/packages/moxplatform_platform_interface/lib/src/contacts.dart @@ -1,14 +1,4 @@ -// The type of icon to use when no avatar path is provided. -enum FallbackIconType { - none(-1), - person(0), - notes(1); - - const FallbackIconType(this.id); - - // The ID of the fallback icon. - final int id; -} +import 'package:moxplatform_platform_interface/src/api.g.dart'; // Wrapper around various contact APIs. // ignore: one_member_abstracts diff --git a/packages/moxplatform_platform_interface/lib/src/contacts_stub.dart b/packages/moxplatform_platform_interface/lib/src/contacts_stub.dart index 51a6dae..939e9c2 100644 --- a/packages/moxplatform_platform_interface/lib/src/contacts_stub.dart +++ b/packages/moxplatform_platform_interface/lib/src/contacts_stub.dart @@ -1,3 +1,4 @@ +import 'package:moxplatform_platform_interface/src/api.g.dart'; import 'package:moxplatform_platform_interface/src/contacts.dart'; class StubContactsImplementation extends ContactsImplementation { diff --git a/pigeons/api.dart b/pigeons/api.dart index 8dd2ca7..a872b24 100644 --- a/pigeons/api.dart +++ b/pigeons/api.dart @@ -161,6 +161,13 @@ class CryptographyResult { final Uint8List ciphertextHash; } +// The type of icon to use when no avatar path is provided. +enum FallbackIconType { + none, + person, + notes; +} + @HostApi() abstract class MoxplatformApi { /// Notification APIs @@ -175,6 +182,9 @@ abstract class MoxplatformApi { String getPersistentDataPath(); String getCacheDataPath(); + /// Contacts APIs + void recordSentMessage(String name, String jid, String? avatarPath, FallbackIconType fallbackIcon); + /// Cryptography APIs @async CryptographyResult? encryptFile(String sourcePath, String destPath, Uint8List key, Uint8List iv, CipherAlgorithm algorithm, String hashSpec); @async CryptographyResult? decryptFile(String sourcePath, String destPath, Uint8List key, Uint8List iv, CipherAlgorithm algorithm, String hashSpec); From 43e88af8038d57b9532c939449b681ea40e730c1 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Fri, 4 Aug 2023 13:38:11 +0200 Subject: [PATCH 20/23] fix: Format and lint --- analysis_options.yaml | 4 + example/lib/main.dart | 60 +++++---- melos.yaml | 4 +- packages/moxplatform/lib/moxplatform.dart | 2 + packages/moxplatform/lib/src/plugin.dart | 3 +- .../lib/src/platform.dart | 1 + .../lib/src/platform_stub.dart | 1 + .../lib/src/contacts_android.dart | 1 - .../lib/src/isolate_android.dart | 1 - .../lib/src/service_android.dart | 1 - .../lib/src/platform_android.dart | 1 + .../lib/src/api.g.dart | 124 ++++++++++++------ .../lib/src/interface.dart | 3 +- .../lib/src/notifications.dart | 7 +- .../lib/src/notifications_stub.dart | 11 +- .../lib/src/platform_stub.dart | 6 +- 16 files changed, 157 insertions(+), 73 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index b3a83ec..1c5c425 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,4 +1,8 @@ include: package:very_good_analysis/analysis_options.yaml +analyzer: + exclude: + - lib/src/api.g.dart + linter: rules: public_member_api_docs: false diff --git a/example/lib/main.dart b/example/lib/main.dart index aabae91..531b21e 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -4,9 +4,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:file_picker/file_picker.dart'; import 'package:moxplatform/moxplatform.dart'; -import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart'; import 'package:permission_handler/permission_handler.dart'; -import 'package:file_picker/file_picker.dart'; /// The id of the notification channel. const channelId = "me.polynom.moxplatform.testing3"; @@ -63,7 +61,10 @@ class MyAppState extends State { ); MoxplatformPlugin.notifications.getEventStream().listen((event) { - print('NotificationEvent(type: ${event.type}, jid: ${event.jid}, payload: ${event.payload}, extras: ${event.extra})'); + // ignore: avoid_print + print( + 'NotificationEvent(type: ${event.type}, jid: ${event.jid}, payload: ${event.payload}, extras: ${event.extra})', + ); }); } @@ -74,14 +75,19 @@ class MyAppState extends State { theme: ThemeData( primarySwatch: Colors.blue, ), - home: MyHomePage(), + home: const MyHomePage(), ); } } -class MyHomePage extends StatelessWidget { - MyHomePage({super.key}); +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key}); + @override + MyHomePageState createState() => MyHomePageState(); +} + +class MyHomePageState extends State { /// List of "Message senders". final List senders = const [ Sender('Mash Kyrielight', 'mash@example.org'), @@ -90,7 +96,8 @@ class MyHomePage extends StatelessWidget { ]; /// List of sent messages. - List messages = List.empty(growable: true); + List messages = + List.empty(growable: true); Future _cryptoTest() async { final result = await FilePicker.platform.pickFiles(); @@ -111,10 +118,13 @@ class MyHomePage extends StatelessWidget { final end = DateTime.now(); final diff = end.millisecondsSinceEpoch - start.millisecondsSinceEpoch; + // ignore: avoid_print print('TIME: ${diff / 1000}s'); + // ignore: avoid_print print('DONE (${enc != null})'); final lengthEnc = await File('$path.enc').length(); final lengthOrig = await File(path).length(); + // ignore: avoid_print print('Encrypted file is $lengthEnc Bytes large (Orig $lengthOrig)'); await MoxplatformPlugin.crypto.decryptFile( @@ -125,9 +135,11 @@ class MyHomePage extends StatelessWidget { CipherAlgorithm.aes256CbcPkcs7, 'SHA-256', ); + // ignore: avoid_print print('DONE'); final lengthDec = await File('$path.dec').length(); + // ignore: avoid_print print('Decrypted file is $lengthDec Bytes large (Orig $lengthOrig)'); } @@ -152,13 +164,15 @@ class MyHomePage extends StatelessWidget { ), ElevatedButton( onPressed: () { - MoxplatformPlugin.contacts.recordSentMessage('Person', 'Person', fallbackIcon: FallbackIconType.person); + MoxplatformPlugin.contacts.recordSentMessage('Person', 'Person', + fallbackIcon: FallbackIconType.person); }, child: const Text('Test recordSentMessage (person fallback)'), ), ElevatedButton( onPressed: () { - MoxplatformPlugin.contacts.recordSentMessage('Notes', 'Notes', fallbackIcon: FallbackIconType.notes); + MoxplatformPlugin.contacts.recordSentMessage('Notes', 'Notes', + fallbackIcon: FallbackIconType.notes); }, child: const Text('Test recordSentMessage (notes fallback)'), ), @@ -167,23 +181,22 @@ class MyHomePage extends StatelessWidget { final result = await FilePicker.platform.pickFiles( type: FileType.image, ); + // ignore: avoid_print print('Picked file: ${result?.files.single.path}'); // Create a new message. final senderIndex = Random().nextInt(senders.length); final time = DateTime.now().millisecondsSinceEpoch; - messages.add( - NotificationMessage( - jid: senders[senderIndex].jid, - sender: senders[senderIndex].name, - content: NotificationMessageContent( - body: result != null ? null : 'Message #${messages.length}', - mime: 'image/jpeg', - path: result?.files.single.path, - ), - timestamp: time, - ) - ); + messages.add(NotificationMessage( + jid: senders[senderIndex].jid, + sender: senders[senderIndex].name, + content: NotificationMessageContent( + body: result != null ? null : 'Message #${messages.length}', + mime: 'image/jpeg', + path: result?.files.single.path, + ), + timestamp: time, + )); await Future.delayed(const Duration(seconds: 4)); await MoxplatformPlugin.notifications.showMessagingNotification( @@ -239,18 +252,21 @@ class MyHomePage extends StatelessWidget { ); if (result == null) return; - MoxplatformPlugin.notifications.setNotificationSelfAvatar(result.files.single.path!); + MoxplatformPlugin.notifications + .setNotificationSelfAvatar(result.files.single.path!); }, child: const Text('Set notification self-avatar'), ), ElevatedButton( onPressed: () async { + // ignore: avoid_print print(await MoxplatformPlugin.platform.getPersistentDataPath()); }, child: const Text('Get data directory'), ), ElevatedButton( onPressed: () async { + // ignore: avoid_print print(await MoxplatformPlugin.platform.getCacheDataPath()); }, child: const Text('Get cache directory'), diff --git a/melos.yaml b/melos.yaml index 68d7a73..da5358e 100644 --- a/melos.yaml +++ b/melos.yaml @@ -8,5 +8,7 @@ command: usePubspecOverrides: true scripts: + format: + exec: dart format . analyze: - exec: dart analyze . + exec: flutter analyze diff --git a/packages/moxplatform/lib/moxplatform.dart b/packages/moxplatform/lib/moxplatform.dart index 01308d4..f2cdfc6 100644 --- a/packages/moxplatform/lib/moxplatform.dart +++ b/packages/moxplatform/lib/moxplatform.dart @@ -1,4 +1,6 @@ library moxplatform; +export 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart'; + export 'src/plugin.dart'; export 'src/types.dart'; diff --git a/packages/moxplatform/lib/src/plugin.dart b/packages/moxplatform/lib/src/plugin.dart index 39853c2..b2dbe6b 100644 --- a/packages/moxplatform/lib/src/plugin.dart +++ b/packages/moxplatform/lib/src/plugin.dart @@ -4,6 +4,7 @@ class MoxplatformPlugin { static IsolateHandler get handler => MoxplatformInterface.handler; static CryptographyImplementation get crypto => MoxplatformInterface.crypto; static ContactsImplementation get contacts => MoxplatformInterface.contacts; - static NotificationsImplementation get notifications => MoxplatformInterface.notifications; + static NotificationsImplementation get notifications => + MoxplatformInterface.notifications; static PlatformImplementation get platform => MoxplatformInterface.platform; } diff --git a/packages/moxplatform/packages/moxplatform_platform_interface/lib/src/platform.dart b/packages/moxplatform/packages/moxplatform_platform_interface/lib/src/platform.dart index e69de29..8b13789 100644 --- a/packages/moxplatform/packages/moxplatform_platform_interface/lib/src/platform.dart +++ b/packages/moxplatform/packages/moxplatform_platform_interface/lib/src/platform.dart @@ -0,0 +1 @@ + diff --git a/packages/moxplatform/packages/moxplatform_platform_interface/lib/src/platform_stub.dart b/packages/moxplatform/packages/moxplatform_platform_interface/lib/src/platform_stub.dart index e69de29..8b13789 100644 --- a/packages/moxplatform/packages/moxplatform_platform_interface/lib/src/platform_stub.dart +++ b/packages/moxplatform/packages/moxplatform_platform_interface/lib/src/platform_stub.dart @@ -0,0 +1 @@ + diff --git a/packages/moxplatform_android/lib/src/contacts_android.dart b/packages/moxplatform_android/lib/src/contacts_android.dart index c21ea4a..b9d2b9d 100644 --- a/packages/moxplatform_android/lib/src/contacts_android.dart +++ b/packages/moxplatform_android/lib/src/contacts_android.dart @@ -1,5 +1,4 @@ import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart'; -import 'package:moxplatform_platform_interface/src/api.g.dart'; class AndroidContactsImplementation extends ContactsImplementation { final MoxplatformApi _api = MoxplatformApi(); diff --git a/packages/moxplatform_android/lib/src/isolate_android.dart b/packages/moxplatform_android/lib/src/isolate_android.dart index fd14e54..2675c49 100644 --- a/packages/moxplatform_android/lib/src/isolate_android.dart +++ b/packages/moxplatform_android/lib/src/isolate_android.dart @@ -7,7 +7,6 @@ import 'package:logging/logging.dart'; import 'package:moxlib/moxlib.dart'; import 'package:moxplatform/moxplatform.dart'; import 'package:moxplatform_android/src/service_android.dart'; -import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart'; /// An [AwaitableDataSender] that uses flutter_background_service. class BackgroundServiceDataSender diff --git a/packages/moxplatform_android/lib/src/service_android.dart b/packages/moxplatform_android/lib/src/service_android.dart index 46d62b8..23faa77 100644 --- a/packages/moxplatform_android/lib/src/service_android.dart +++ b/packages/moxplatform_android/lib/src/service_android.dart @@ -6,7 +6,6 @@ import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; import 'package:moxlib/moxlib.dart'; import 'package:moxplatform/moxplatform.dart'; -import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart'; import 'package:uuid/uuid.dart'; class AndroidBackgroundService extends BackgroundService { diff --git a/packages/moxplatform_android/packages/moxplatform_android/lib/src/platform_android.dart b/packages/moxplatform_android/packages/moxplatform_android/lib/src/platform_android.dart index e69de29..8b13789 100644 --- a/packages/moxplatform_android/packages/moxplatform_android/lib/src/platform_android.dart +++ b/packages/moxplatform_android/packages/moxplatform_android/lib/src/platform_android.dart @@ -0,0 +1 @@ + diff --git a/packages/moxplatform_platform_interface/lib/src/api.g.dart b/packages/moxplatform_platform_interface/lib/src/api.g.dart index 0725d2e..81ecb15 100644 --- a/packages/moxplatform_platform_interface/lib/src/api.g.dart +++ b/packages/moxplatform_platform_interface/lib/src/api.g.dart @@ -356,19 +356,19 @@ class _MoxplatformApiCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return CryptographyResult.decode(readValue(buffer)!); - case 129: + case 129: return MessagingNotification.decode(readValue(buffer)!); - case 130: + case 130: return NotificationEvent.decode(readValue(buffer)!); - case 131: + case 131: return NotificationI18nData.decode(readValue(buffer)!); - case 132: + case 132: return NotificationMessage.decode(readValue(buffer)!); - case 133: + case 133: return NotificationMessageContent.decode(readValue(buffer)!); - case 134: + case 134: return RegularNotification.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -387,12 +387,15 @@ class MoxplatformApi { static const MessageCodec codec = _MoxplatformApiCodec(); /// Notification APIs - Future createNotificationChannel(String arg_title, String arg_description, String arg_id, bool arg_urgent) async { + Future createNotificationChannel(String arg_title, + String arg_description, String arg_id, bool arg_urgent) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.createNotificationChannel', codec, + 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.createNotificationChannel', + codec, binaryMessenger: _binaryMessenger); - final List? replyList = - await channel.send([arg_title, arg_description, arg_id, arg_urgent]) as List?; + final List? replyList = await channel + .send([arg_title, arg_description, arg_id, arg_urgent]) + as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -409,9 +412,11 @@ class MoxplatformApi { } } - Future showMessagingNotification(MessagingNotification arg_notification) async { + Future showMessagingNotification( + MessagingNotification arg_notification) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.showMessagingNotification', codec, + 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.showMessagingNotification', + codec, binaryMessenger: _binaryMessenger); final List? replyList = await channel.send([arg_notification]) as List?; @@ -433,7 +438,8 @@ class MoxplatformApi { Future showNotification(RegularNotification arg_notification) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.showNotification', codec, + 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.showNotification', + codec, binaryMessenger: _binaryMessenger); final List? replyList = await channel.send([arg_notification]) as List?; @@ -455,7 +461,8 @@ class MoxplatformApi { Future dismissNotification(int arg_id) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.dismissNotification', codec, + 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.dismissNotification', + codec, binaryMessenger: _binaryMessenger); final List? replyList = await channel.send([arg_id]) as List?; @@ -477,7 +484,8 @@ class MoxplatformApi { Future setNotificationSelfAvatar(String arg_path) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.setNotificationSelfAvatar', codec, + 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.setNotificationSelfAvatar', + codec, binaryMessenger: _binaryMessenger); final List? replyList = await channel.send([arg_path]) as List?; @@ -499,7 +507,8 @@ class MoxplatformApi { Future setNotificationI18n(NotificationI18nData arg_data) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.setNotificationI18n', codec, + 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.setNotificationI18n', + codec, binaryMessenger: _binaryMessenger); final List? replyList = await channel.send([arg_data]) as List?; @@ -522,10 +531,10 @@ class MoxplatformApi { /// Platform APIs Future getPersistentDataPath() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.getPersistentDataPath', codec, + 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.getPersistentDataPath', + 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', @@ -549,10 +558,10 @@ class MoxplatformApi { Future getCacheDataPath() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.getCacheDataPath', codec, + 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.getCacheDataPath', + 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', @@ -575,12 +584,18 @@ class MoxplatformApi { } /// Contacts APIs - Future recordSentMessage(String arg_name, String arg_jid, String? arg_avatarPath, FallbackIconType arg_fallbackIcon) async { + Future recordSentMessage(String arg_name, String arg_jid, + String? arg_avatarPath, FallbackIconType arg_fallbackIcon) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.recordSentMessage', codec, + 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.recordSentMessage', + codec, binaryMessenger: _binaryMessenger); - final List? replyList = - await channel.send([arg_name, arg_jid, arg_avatarPath, arg_fallbackIcon.index]) as List?; + final List? replyList = await channel.send([ + arg_name, + arg_jid, + arg_avatarPath, + arg_fallbackIcon.index + ]) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -598,12 +613,25 @@ class MoxplatformApi { } /// Cryptography APIs - Future encryptFile(String arg_sourcePath, String arg_destPath, Uint8List arg_key, Uint8List arg_iv, CipherAlgorithm arg_algorithm, String arg_hashSpec) async { + Future encryptFile( + String arg_sourcePath, + String arg_destPath, + Uint8List arg_key, + Uint8List arg_iv, + CipherAlgorithm arg_algorithm, + String arg_hashSpec) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.encryptFile', codec, + 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.encryptFile', + codec, binaryMessenger: _binaryMessenger); - final List? replyList = - await channel.send([arg_sourcePath, arg_destPath, arg_key, arg_iv, arg_algorithm.index, arg_hashSpec]) as List?; + final List? replyList = await channel.send([ + arg_sourcePath, + arg_destPath, + arg_key, + arg_iv, + arg_algorithm.index, + arg_hashSpec + ]) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -620,12 +648,25 @@ class MoxplatformApi { } } - Future decryptFile(String arg_sourcePath, String arg_destPath, Uint8List arg_key, Uint8List arg_iv, CipherAlgorithm arg_algorithm, String arg_hashSpec) async { + Future decryptFile( + String arg_sourcePath, + String arg_destPath, + Uint8List arg_key, + Uint8List arg_iv, + CipherAlgorithm arg_algorithm, + String arg_hashSpec) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.decryptFile', codec, + 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.decryptFile', + codec, binaryMessenger: _binaryMessenger); - final List? replyList = - await channel.send([arg_sourcePath, arg_destPath, arg_key, arg_iv, arg_algorithm.index, arg_hashSpec]) as List?; + final List? replyList = await channel.send([ + arg_sourcePath, + arg_destPath, + arg_key, + arg_iv, + arg_algorithm.index, + arg_hashSpec + ]) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -642,12 +683,14 @@ class MoxplatformApi { } } - Future hashFile(String arg_sourcePath, String arg_hashSpec) async { + Future hashFile( + String arg_sourcePath, String arg_hashSpec) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.hashFile', codec, + 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.hashFile', + codec, binaryMessenger: _binaryMessenger); - final List? replyList = - await channel.send([arg_sourcePath, arg_hashSpec]) as List?; + final List? replyList = await channel + .send([arg_sourcePath, arg_hashSpec]) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -667,7 +710,8 @@ class MoxplatformApi { /// Stubs Future eventStub(NotificationEvent arg_event) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.eventStub', codec, + 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.eventStub', + codec, binaryMessenger: _binaryMessenger); final List? replyList = await channel.send([arg_event]) as List?; diff --git a/packages/moxplatform_platform_interface/lib/src/interface.dart b/packages/moxplatform_platform_interface/lib/src/interface.dart index 45fd5d2..b5ec7c6 100644 --- a/packages/moxplatform_platform_interface/lib/src/interface.dart +++ b/packages/moxplatform_platform_interface/lib/src/interface.dart @@ -21,7 +21,8 @@ abstract class MoxplatformInterface extends PlatformInterface { static IsolateHandler handler = StubIsolateHandler(); static CryptographyImplementation crypto = StubCryptographyImplementation(); static ContactsImplementation contacts = StubContactsImplementation(); - static NotificationsImplementation notifications = StubNotificationsImplementation(); + static NotificationsImplementation notifications = + StubNotificationsImplementation(); static PlatformImplementation platform = StubPlatformImplementation(); /// Return the current platform name. diff --git a/packages/moxplatform_platform_interface/lib/src/notifications.dart b/packages/moxplatform_platform_interface/lib/src/notifications.dart index 3918bed..cc55742 100644 --- a/packages/moxplatform_platform_interface/lib/src/notifications.dart +++ b/packages/moxplatform_platform_interface/lib/src/notifications.dart @@ -4,7 +4,12 @@ import 'package:moxplatform_platform_interface/src/api.g.dart'; abstract class NotificationsImplementation { /// Creates a notification channel with the name [title] and id [id]. If [urgent] is true, then /// it configures the channel as carrying urgent information. - Future createNotificationChannel(String title, String description, String id, bool urgent); + Future createNotificationChannel( + String title, + String description, + String id, + bool urgent, + ); /// Shows a notification [notification] in the messaging style with everyting it needs. Future showMessagingNotification(MessagingNotification notification); diff --git a/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart b/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart index 0fe38d4..af3abb1 100644 --- a/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart +++ b/packages/moxplatform_platform_interface/lib/src/notifications_stub.dart @@ -4,10 +4,17 @@ import 'package:moxplatform_platform_interface/src/notifications.dart'; class StubNotificationsImplementation extends NotificationsImplementation { @override - Future createNotificationChannel(String title, String description, String id, bool urgent) async {} + Future createNotificationChannel( + String title, + String description, + String id, + bool urgent, + ) async {} @override - Future showMessagingNotification(MessagingNotification notification) async {} + Future showMessagingNotification( + MessagingNotification notification, + ) async {} @override Future showNotification(RegularNotification notification) async {} diff --git a/packages/moxplatform_platform_interface/lib/src/platform_stub.dart b/packages/moxplatform_platform_interface/lib/src/platform_stub.dart index ad85004..eeb11ea 100644 --- a/packages/moxplatform_platform_interface/lib/src/platform_stub.dart +++ b/packages/moxplatform_platform_interface/lib/src/platform_stub.dart @@ -2,8 +2,10 @@ import 'package:moxplatform_platform_interface/src/platform.dart'; class StubPlatformImplementation extends PlatformImplementation { /// Returns the path where persistent data should be stored. - Future getPersistentDataPath() async => ""; + @override + Future getPersistentDataPath() async => ''; /// Returns the path where cache data should be stored. - Future getCacheDataPath() async => ""; + @override + Future getCacheDataPath() async => ''; } From 960bad46d4a67404dcb93d511bcf88c19efdcd8e Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Fri, 4 Aug 2023 13:43:42 +0200 Subject: [PATCH 21/23] docs: Update docs a little --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a6d5a48..ffbed1b 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,11 @@ This repo is based on [very_good_flutter_plugin](https://github.com/VeryGoodOpen The development of this package is based on [melos](https://pub.dev/packages/melos). To make all packages link to each other locally, begin by running `melos bootstrap`. After editing -the code and making your changes, please run `melos run analyze` to make sure that no linter warnings -are left inside the code. +the code and making your changes, please format the code using `melos run format` and lint using `melos run analyze`. + +When done - and a version bump is appropriate - bump the version of all packages using `melos version` and +publish with `melos publish --no-dry-run --git-tag-version`. ## Acknowledgements -- [ekasetiawans](https://github.com/ekasetiawans) for [flutter_background_service](https://github.com/ekasetiawans/flutter_background_service). moxplatform_android is basically just a copy and paste of [flutter_background_service](https://github.com/ekasetiawans/flutter_background_service). +- [ekasetiawans](https://github.com/ekasetiawans) for [flutter_background_service](https://github.com/ekasetiawans/flutter_background_service). moxplatform_android's service implementation is basically just a copy and paste of [flutter_background_service](https://github.com/ekasetiawans/flutter_background_service). From 173d5f5166c591393b334e977da115c6ca3b06fc Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Fri, 4 Aug 2023 13:44:52 +0200 Subject: [PATCH 22/23] chore(release): publish packages - moxplatform@0.1.17+2 - moxplatform_android@0.1.18 - moxplatform_platform_interface@0.1.18 --- CHANGELOG.md | 54 +++++++++++++++++++ example/pubspec.yaml | 4 +- packages/moxplatform/CHANGELOG.md | 4 ++ packages/moxplatform/pubspec.yaml | 6 +-- packages/moxplatform_android/CHANGELOG.md | 18 +++++++ packages/moxplatform_android/pubspec.yaml | 6 +-- .../CHANGELOG.md | 13 +++++ .../pubspec.yaml | 4 +- 8 files changed, 99 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d2f5c8..1df485e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,60 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## 2023-08-04 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`moxplatform` - `v0.1.17+2`](#moxplatform---v01172) + - [`moxplatform_android` - `v0.1.18`](#moxplatform_android---v0118) + - [`moxplatform_platform_interface` - `v0.1.18`](#moxplatform_platform_interface---v0118) + +--- + +#### `moxplatform` - `v0.1.17+2` + + - **FIX**: Format and lint. + +#### `moxplatform_android` - `v0.1.18` + + - **FIX**: Format and lint. + - **FIX**: Fix self-replies after receiving another message. + - **FIX**: Add payload to all intents. + - **FIX**: Fix images disappearing after replying. + - **FEAT**: Move recordSentMessage to pigeon. + - **FEAT**: Move the crypto APIs to pigeon. + - **FEAT**: Adjust to Moxxy changes. + - **FEAT**: Store the avatar path also in the shared preferences. + - **FEAT**: Allow the sender's data being null. + - **FEAT**: Allow attaching arbitrary data to the notification. + - **FEAT**: Allow showing regular notifications. + - **FEAT**: Make i18n data a bit more persistent. + - **FEAT**: Color in the notification silhouette. + - **FEAT**: Allow setting the self-avatar. + - **FEAT**: Take care of i18n. + +#### `moxplatform_platform_interface` - `v0.1.18` + + - **FIX**: Format and lint. + - **FIX**: Add payload to all intents. + - **FEAT**: Move recordSentMessage to pigeon. + - **FEAT**: Move the crypto APIs to pigeon. + - **FEAT**: Allow the sender's data being null. + - **FEAT**: Allow attaching arbitrary data to the notification. + - **FEAT**: Allow showing regular notifications. + - **FEAT**: Color in the notification silhouette. + - **FEAT**: Allow setting the self-avatar. + - **FEAT**: Take care of i18n. + + ## 2023-07-21 ### Changes diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 955dd3a..3ff4a34 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -32,10 +32,10 @@ dependencies: moxplatform: hosted: https://git.polynom.me/api/packages/Moxxy/pub - version: 0.1.17+1 + version: 0.1.17+2 moxplatform_android: hosted: https://git.polynom.me/api/packages/Moxxy/pub - version: 0.1.17+1 + version: 0.1.18 file_picker: 5.2.0+1 diff --git a/packages/moxplatform/CHANGELOG.md b/packages/moxplatform/CHANGELOG.md index 955acd1..502f23c 100644 --- a/packages/moxplatform/CHANGELOG.md +++ b/packages/moxplatform/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.17+2 + + - **FIX**: Format and lint. + ## 0.1.17+1 - Update a dependency to the latest release. diff --git a/packages/moxplatform/pubspec.yaml b/packages/moxplatform/pubspec.yaml index bf33115..d9907be 100644 --- a/packages/moxplatform/pubspec.yaml +++ b/packages/moxplatform/pubspec.yaml @@ -1,6 +1,6 @@ name: moxplatform description: Moxxy platform-specific code -version: 0.1.17+1 +version: 0.1.17+2 publish_to: https://git.polynom.me/api/packages/Moxxy/pub homepage: https://codeberg.org/moxxy/moxplatform @@ -26,10 +26,10 @@ dependencies: moxplatform_android: hosted: https://git.polynom.me/api/packages/Moxxy/pub - version: ^0.1.17+1 + version: ^0.1.18 moxplatform_platform_interface: hosted: https://git.polynom.me/api/packages/Moxxy/pub - version: ^0.1.17+1 + version: ^0.1.18 dev_dependencies: flutter_test: diff --git a/packages/moxplatform_android/CHANGELOG.md b/packages/moxplatform_android/CHANGELOG.md index e55925c..bc077ba 100644 --- a/packages/moxplatform_android/CHANGELOG.md +++ b/packages/moxplatform_android/CHANGELOG.md @@ -1,3 +1,21 @@ +## 0.1.18 + + - **FIX**: Format and lint. + - **FIX**: Fix self-replies after receiving another message. + - **FIX**: Add payload to all intents. + - **FIX**: Fix images disappearing after replying. + - **FEAT**: Move recordSentMessage to pigeon. + - **FEAT**: Move the crypto APIs to pigeon. + - **FEAT**: Adjust to Moxxy changes. + - **FEAT**: Store the avatar path also in the shared preferences. + - **FEAT**: Allow the sender's data being null. + - **FEAT**: Allow attaching arbitrary data to the notification. + - **FEAT**: Allow showing regular notifications. + - **FEAT**: Make i18n data a bit more persistent. + - **FEAT**: Color in the notification silhouette. + - **FEAT**: Allow setting the self-avatar. + - **FEAT**: Take care of i18n. + ## 0.1.17+1 - **FIX**: Accidentally used the name as the target's key. Oops. diff --git a/packages/moxplatform_android/pubspec.yaml b/packages/moxplatform_android/pubspec.yaml index e9b12ff..0ac4276 100644 --- a/packages/moxplatform_android/pubspec.yaml +++ b/packages/moxplatform_android/pubspec.yaml @@ -1,6 +1,6 @@ name: moxplatform_android description: Android implementation of moxplatform -version: 0.1.17+1 +version: 0.1.18 homepage: https://codeberg.org/moxxy/moxplatform publish_to: https://git.polynom.me/api/packages/Moxxy/pub @@ -29,10 +29,10 @@ dependencies: moxplatform: hosted: https://git.polynom.me/api/packages/Moxxy/pub - version: ^0.1.17+1 + version: ^0.1.17+2 moxplatform_platform_interface: hosted: https://git.polynom.me/api/packages/Moxxy/pub - version: ^0.1.17+1 + version: ^0.1.18 plugin_platform_interface: ^2.1.2 uuid: ^3.0.5 diff --git a/packages/moxplatform_platform_interface/CHANGELOG.md b/packages/moxplatform_platform_interface/CHANGELOG.md index ba9b8b4..33d42c5 100644 --- a/packages/moxplatform_platform_interface/CHANGELOG.md +++ b/packages/moxplatform_platform_interface/CHANGELOG.md @@ -1,3 +1,16 @@ +## 0.1.18 + + - **FIX**: Format and lint. + - **FIX**: Add payload to all intents. + - **FEAT**: Move recordSentMessage to pigeon. + - **FEAT**: Move the crypto APIs to pigeon. + - **FEAT**: Allow the sender's data being null. + - **FEAT**: Allow attaching arbitrary data to the notification. + - **FEAT**: Allow showing regular notifications. + - **FEAT**: Color in the notification silhouette. + - **FEAT**: Allow setting the self-avatar. + - **FEAT**: Take care of i18n. + ## 0.1.17+1 - Update a dependency to the latest release. diff --git a/packages/moxplatform_platform_interface/pubspec.yaml b/packages/moxplatform_platform_interface/pubspec.yaml index 3bc8034..65795aa 100644 --- a/packages/moxplatform_platform_interface/pubspec.yaml +++ b/packages/moxplatform_platform_interface/pubspec.yaml @@ -1,6 +1,6 @@ name: moxplatform_platform_interface description: A common platform interface for the my_plugin plugin. -version: 0.1.17+1 +version: 0.1.18 homepage: https://codeberg.org/moxxy/moxplatform publish_to: https://git.polynom.me/api/packages/Moxxy/pub @@ -17,7 +17,7 @@ dependencies: version: ^0.2.0 moxplatform: hosted: https://git.polynom.me/api/packages/Moxxy/pub - version: ^0.1.17+1 + version: ^0.1.17+2 plugin_platform_interface: ^2.1.2 From 3c773e5270501dad9a392a560e0eb05422b753bc Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Fri, 4 Aug 2023 13:50:32 +0200 Subject: [PATCH 23/23] chore(repo): Introduce gitlint --- .gitlint | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .gitlint diff --git a/.gitlint b/.gitlint new file mode 100644 index 0000000..1d74fb6 --- /dev/null +++ b/.gitlint @@ -0,0 +1,14 @@ +[general] +ignore=B5,B6,B7,B8 + +[title-max-length] +line-length=72 + +[title-trailing-punctuation] +[title-hard-tab] +[title-match-regex] +regex=^(feat|fix|chore|refactor)\((android|ios|linux|windows|macos|interface|base|repo)(,(android|ios|linux|windows|macos|interface|base))*\): [A-Z0-9].*$ + + +[body-trailing-whitespace] +[body-first-line-empty]