feat: Take care of i18n

This commit is contained in:
PapaTutuWawa 2023-07-28 17:32:14 +02:00
parent f90b3866ab
commit adb8ee88d1
12 changed files with 277 additions and 52 deletions

View File

@ -41,7 +41,16 @@ class MyAppState extends State<MyApp> {
Future<void> 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})');

View File

@ -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<Object> toList() {
ArrayList<Object> toListResult = new ArrayList<Object>(3);
toListResult.add(reply);
toListResult.add(markAsRead);
toListResult.add(you);
return toListResult;
}
static @NonNull NotificationI18nData fromList(@NonNull ArrayList<Object> 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<Object>) readValue(buffer));
case (byte) 130:
return NotificationMessage.fromList((ArrayList<Object>) readValue(buffer));
return NotificationI18nData.fromList((ArrayList<Object>) readValue(buffer));
case (byte) 131:
return NotificationMessage.fromList((ArrayList<Object>) readValue(buffer));
case (byte) 132:
return NotificationMessageContent.fromList((ArrayList<Object>) 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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <Object?>[
reply,
markAsRead,
you,
];
}
static NotificationI18nData decode(Object result) {
result as List<Object?>;
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<Object?> codec = _MoxplatformApiCodec();
Future<void> createNotificationChannel(String arg_title, String arg_id, bool arg_urgent) async {
Future<void> createNotificationChannel(String arg_title, String arg_id, bool arg_urgent, NotificationI18nData arg_i18n) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.createNotificationChannel', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_title, arg_id, arg_urgent]) as List<Object?>?;
await channel.send(<Object?>[arg_title, arg_id, arg_urgent, arg_i18n]) as List<Object?>?;
if (replyList == null) {
throw PlatformException(
code: 'channel-error',

View File

@ -2,7 +2,7 @@ import 'dart:async';
import 'package:moxplatform_platform_interface/src/api.g.dart';
abstract class NotificationsImplementation {
Future<void> createNotificationChannel(String title, String id, bool urgent);
Future<void> createNotificationChannel(String title, String id, bool urgent, NotificationI18nData i18n);
Future<void> showMessagingNotification(MessagingNotification notification);

View File

@ -4,7 +4,7 @@ import 'package:moxplatform_platform_interface/src/notifications.dart';
class StubNotificationsImplementation extends NotificationsImplementation {
@override
Future<void> createNotificationChannel(String title, String id, bool urgent) async {}
Future<void> createNotificationChannel(String title, String id, bool urgent, NotificationI18nData i18n) async {}
@override
Future<void> showMessagingNotification(MessagingNotification notification) async {}

View File

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