Skip to content

notif: Support messaging-style notifications #718

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 169 additions & 3 deletions android/app/src/main/kotlin/com/zulip/flutter/Notifications.g.kt
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,112 @@ data class InboxStyle (
)
}
}

/**
* Corresponds to `androidx.core.app.Person`
*
* See: https://developer.android.com/reference/androidx/core/app/Person
*
* Generated class from Pigeon that represents data sent in messages.
*/
data class Person (
/**
* An icon for this person.
*
* This should be compressed image data, in a format to be passed
* to `androidx.core.graphics.drawable.IconCompat.createWithData`.
* Supported formats include JPEG, PNG, and WEBP.
*
* See:
* https://developer.android.com/reference/androidx/core/graphics/drawable/IconCompat#createWithData(byte[],int,int)
*/
val iconBitmap: ByteArray? = null,
val key: String,
val name: String

) {
companion object {
@Suppress("LocalVariableName")
fun fromList(__pigeon_list: List<Any?>): Person {
val iconBitmap = __pigeon_list[0] as ByteArray?
val key = __pigeon_list[1] as String
val name = __pigeon_list[2] as String
return Person(iconBitmap, key, name)
}
}
fun toList(): List<Any?> {
return listOf(
iconBitmap,
key,
name,
)
}
}

/**
* Corresponds to `androidx.core.app.NotificationCompat.MessagingStyle.Message`
*
* See: https://developer.android.com/reference/androidx/core/app/NotificationCompat.MessagingStyle.Message
*
* Generated class from Pigeon that represents data sent in messages.
*/
data class MessagingStyleMessage (
val text: String,
val timestampMs: Long,
val person: Person

) {
companion object {
@Suppress("LocalVariableName")
fun fromList(__pigeon_list: List<Any?>): MessagingStyleMessage {
val text = __pigeon_list[0] as String
val timestampMs = __pigeon_list[1].let { num -> if (num is Int) num.toLong() else num as Long }
val person = __pigeon_list[2] as Person
return MessagingStyleMessage(text, timestampMs, person)
}
}
fun toList(): List<Any?> {
return listOf(
text,
timestampMs,
person,
)
}
}

/**
* Corresponds to `androidx.core.app.NotificationCompat.MessagingStyle`
*
* See: https://developer.android.com/reference/androidx/core/app/NotificationCompat.MessagingStyle
*
* Generated class from Pigeon that represents data sent in messages.
*/
data class MessagingStyle (
val user: Person,
val conversationTitle: String? = null,
val messages: List<MessagingStyleMessage?>,
val isGroupConversation: Boolean

) {
companion object {
@Suppress("LocalVariableName")
fun fromList(__pigeon_list: List<Any?>): MessagingStyle {
val user = __pigeon_list[0] as Person
val conversationTitle = __pigeon_list[1] as String?
val messages = __pigeon_list[2] as List<MessagingStyleMessage?>
val isGroupConversation = __pigeon_list[3] as Boolean
return MessagingStyle(user, conversationTitle, messages, isGroupConversation)
}
}
fun toList(): List<Any?> {
return listOf(
user,
conversationTitle,
messages,
isGroupConversation,
)
}
}
private object NotificationsPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return when (type) {
Expand All @@ -120,6 +226,21 @@ private object NotificationsPigeonCodec : StandardMessageCodec() {
InboxStyle.fromList(it)
}
}
131.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
Person.fromList(it)
}
}
132.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
MessagingStyleMessage.fromList(it)
}
}
133.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
MessagingStyle.fromList(it)
}
}
else -> super.readValueOfType(type, buffer)
}
}
Expand All @@ -133,6 +254,18 @@ private object NotificationsPigeonCodec : StandardMessageCodec() {
stream.write(130)
writeValue(stream, value.toList())
}
is Person -> {
stream.write(131)
writeValue(stream, value.toList())
}
is MessagingStyleMessage -> {
stream.write(132)
writeValue(stream, value.toList())
}
is MessagingStyle -> {
stream.write(133)
writeValue(stream, value.toList())
}
else -> super.writeValue(stream, value)
}
}
Expand All @@ -159,7 +292,21 @@ interface AndroidNotificationHostApi {
* https://developer.android.com/reference/kotlin/android/app/NotificationManager.html#notify
* https://developer.android.com/reference/androidx/core/app/NotificationCompat.Builder
*/
fun notify(tag: String?, id: Long, autoCancel: Boolean?, channelId: String, color: Long?, contentIntent: PendingIntent?, contentText: String?, contentTitle: String?, extras: Map<String?, String?>?, groupKey: String?, inboxStyle: InboxStyle?, isGroupSummary: Boolean?, smallIconResourceName: String?)
fun notify(tag: String?, id: Long, autoCancel: Boolean?, channelId: String, color: Long?, contentIntent: PendingIntent?, contentText: String?, contentTitle: String?, extras: Map<String?, String?>?, groupKey: String?, inboxStyle: InboxStyle?, isGroupSummary: Boolean?, messagingStyle: MessagingStyle?, number: Long?, smallIconResourceName: String?)
/**
* Wraps `androidx.core.app.NotificationManagerCompat.getActiveNotifications`,
* combined with `androidx.core.app.NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification`.
*
* Returns the messaging style, if any, of an active notification
* that has tag `tag`. If there are several such notifications,
* an arbitrary one of them is used.
* Returns null if there are no such notifications.
*
* See:
* https://developer.android.com/reference/kotlin/androidx/core/app/NotificationManagerCompat#getActiveNotifications()
* https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat.MessagingStyle#extractMessagingStyleFromNotification(android.app.Notification)
*/
fun getActiveNotificationMessagingStyleByTag(tag: String): MessagingStyle?

companion object {
/** The codec used by AndroidNotificationHostApi. */
Expand Down Expand Up @@ -187,9 +334,11 @@ interface AndroidNotificationHostApi {
val groupKeyArg = args[9] as String?
val inboxStyleArg = args[10] as InboxStyle?
val isGroupSummaryArg = args[11] as Boolean?
val smallIconResourceNameArg = args[12] as String?
val messagingStyleArg = args[12] as MessagingStyle?
val numberArg = args[13].let { num -> if (num is Int) num.toLong() else num as Long? }
val smallIconResourceNameArg = args[14] as String?
val wrapped: List<Any?> = try {
api.notify(tagArg, idArg, autoCancelArg, channelIdArg, colorArg, contentIntentArg, contentTextArg, contentTitleArg, extrasArg, groupKeyArg, inboxStyleArg, isGroupSummaryArg, smallIconResourceNameArg)
api.notify(tagArg, idArg, autoCancelArg, channelIdArg, colorArg, contentIntentArg, contentTextArg, contentTitleArg, extrasArg, groupKeyArg, inboxStyleArg, isGroupSummaryArg, messagingStyleArg, numberArg, smallIconResourceNameArg)
listOf(null)
} catch (exception: Throwable) {
wrapError(exception)
Expand All @@ -200,6 +349,23 @@ interface AndroidNotificationHostApi {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.zulip.AndroidNotificationHostApi.getActiveNotificationMessagingStyleByTag$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val tagArg = args[0] as String
val wrapped: List<Any?> = try {
listOf(api.getActiveNotificationMessagingStyleByTag(tagArg))
} catch (exception: Throwable) {
wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}
67 changes: 67 additions & 0 deletions android/app/src/main/kotlin/com/zulip/flutter/ZulipPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,38 @@ import android.util.Log
import androidx.annotation.Keep
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.graphics.drawable.IconCompat
import io.flutter.embedding.engine.plugins.FlutterPlugin

private const val TAG = "ZulipPlugin"

fun toAndroidPerson(person: Person): androidx.core.app.Person {
return androidx.core.app.Person.Builder().apply {
person.iconBitmap?.let { setIcon(IconCompat.createWithData(it, 0, it.size)) }
setKey(person.key)
setName(person.name)
}.build()
}

fun toPigeonPerson(person: androidx.core.app.Person): Person {
return Person(
// The API doesn't provide a way to retrieve the icon data,
// so we set this to null.
//
// Notably, Android retains a limited number [1] of messages
// in the messaging style, and it also retains the icon data
// for persons within those messages. Therefore, there's no
// need to include the person's icon data in each message.
// Only one icon data instance is needed for each unique
// person's key in the retained messages.
//
// [1]: https://developer.android.com/reference/androidx/core/app/NotificationCompat.MessagingStyle#MAXIMUM_RETAINED_MESSAGES()
null,
person.key!!,
person.name!!.toString(),
)
}

private class AndroidNotificationHost(val context: Context)
: AndroidNotificationHostApi {
@SuppressLint(
Expand All @@ -33,6 +61,8 @@ private class AndroidNotificationHost(val context: Context)
groupKey: String?,
inboxStyle: InboxStyle?,
isGroupSummary: Boolean?,
messagingStyle: MessagingStyle?,
number: Long?,
smallIconResourceName: String?
) {
val notification = NotificationCompat.Builder(context, channelId).apply {
Expand Down Expand Up @@ -60,11 +90,48 @@ private class AndroidNotificationHost(val context: Context)
.setSummaryText(it.summaryText)
) }
isGroupSummary?.let { setGroupSummary(it) }
messagingStyle?.let { messagingStyle ->
val style = NotificationCompat.MessagingStyle(toAndroidPerson(messagingStyle.user))
.setConversationTitle(messagingStyle.conversationTitle)
.setGroupConversation(messagingStyle.isGroupConversation)
messagingStyle.messages.forEach { it?.let {
style.addMessage(NotificationCompat.MessagingStyle.Message(
it.text,
it.timestampMs,
toAndroidPerson(it.person),
))
} }
setStyle(style)
}
number?.let { setNumber(it.toInt()) }
smallIconResourceName?.let { setSmallIcon(context.resources.getIdentifier(
it, "drawable", context.packageName)) }
}.build()
NotificationManagerCompat.from(context).notify(tag, id.toInt(), notification)
}

override fun getActiveNotificationMessagingStyleByTag(tag: String): MessagingStyle? {
val activeNotification = NotificationManagerCompat.from(context)
.activeNotifications
.find { it.tag == tag }
activeNotification?.notification?.let { notification ->
NotificationCompat.MessagingStyle
.extractMessagingStyleFromNotification(notification)
?.let { style ->
return MessagingStyle(
toPigeonPerson(style.user),
style.conversationTitle!!.toString(),
style.messages.map { MessagingStyleMessage(
it.text!!.toString(),
it.timestamp,
toPigeonPerson(it.person!!)
) },
style.isGroupConversation,
)
}
}
return null
}
}

/** A Flutter plugin for the Zulip app's ad-hoc needs. */
Expand Down
4 changes: 4 additions & 0 deletions assets/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -479,5 +479,9 @@
"senderFullName": {"type": "String", "example": "Alice"},
"numOthers": {"type": "int", "example": "4"}
}
},
"notifSelfUser": "You",
"@notifSelfUser": {
"description": "Display name for the user themself, to show after replying in an Android notification"
}
}
Loading