Skip to content

Commit fbef1e7

Browse files
notif: Add messaging-style notifications support to Pigeon bindings
Add methods and types for creating messaging style notifications: https://developer.android.com/develop/ui/views/notifications/build-notification#messaging-style
1 parent 74de34c commit fbef1e7

File tree

5 files changed

+519
-5
lines changed

5 files changed

+519
-5
lines changed

android/app/src/main/kotlin/com/zulip/flutter/Notifications.g.kt

Lines changed: 176 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,117 @@ data class InboxStyle (
107107
)
108108
}
109109
}
110+
111+
/**
112+
* Corresponds to `androidx.core.app.Person`
113+
*
114+
* See: https://developer.android.com/reference/androidx/core/app/Person
115+
*
116+
* Generated class from Pigeon that represents data sent in messages.
117+
*/
118+
data class Person (
119+
/**
120+
* An icon for this person.
121+
*
122+
* Wraps `androidx.core.graphics.drawable.IconCompat.createWithData`
123+
* which generates an `IconCompat` from compressed image data.
124+
*
125+
* It uses `android.graphics.BitmapFactory` to decode the image data,
126+
* and the `android.graphics.Bitmap.CompressFormat` enum lists the
127+
* supported image compression formats (JPEG, PNG, WEBP).
128+
*
129+
* See:
130+
* https://developer.android.com/reference/androidx/core/graphics/drawable/IconCompat#createWithData(byte[],int,int)
131+
* https://developer.android.com/reference/android/graphics/BitmapFactory.html
132+
* https://developer.android.com/reference/android/graphics/Bitmap.CompressFormat.html
133+
*/
134+
val iconBitmap: ByteArray? = null,
135+
val key: String,
136+
val name: String
137+
138+
) {
139+
companion object {
140+
@Suppress("LocalVariableName")
141+
fun fromList(__pigeon_list: List<Any?>): Person {
142+
val iconBitmap = __pigeon_list[0] as ByteArray?
143+
val key = __pigeon_list[1] as String
144+
val name = __pigeon_list[2] as String
145+
return Person(iconBitmap, key, name)
146+
}
147+
}
148+
fun toList(): List<Any?> {
149+
return listOf(
150+
iconBitmap,
151+
key,
152+
name,
153+
)
154+
}
155+
}
156+
157+
/**
158+
* Corresponds to `androidx.core.app.NotificationCompat.MessagingStyle.Message`
159+
*
160+
* See: https://developer.android.com/reference/androidx/core/app/NotificationCompat.MessagingStyle.Message
161+
*
162+
* Generated class from Pigeon that represents data sent in messages.
163+
*/
164+
data class MessagingStyleMessage (
165+
val text: String,
166+
val timestampMs: Long,
167+
val person: Person
168+
169+
) {
170+
companion object {
171+
@Suppress("LocalVariableName")
172+
fun fromList(__pigeon_list: List<Any?>): MessagingStyleMessage {
173+
val text = __pigeon_list[0] as String
174+
val timestampMs = __pigeon_list[1].let { num -> if (num is Int) num.toLong() else num as Long }
175+
val person = __pigeon_list[2] as Person
176+
return MessagingStyleMessage(text, timestampMs, person)
177+
}
178+
}
179+
fun toList(): List<Any?> {
180+
return listOf(
181+
text,
182+
timestampMs,
183+
person,
184+
)
185+
}
186+
}
187+
188+
/**
189+
* Corresponds to `androidx.core.app.NotificationCompat.MessagingStyle`
190+
*
191+
* See: https://developer.android.com/reference/androidx/core/app/NotificationCompat.MessagingStyle
192+
*
193+
* Generated class from Pigeon that represents data sent in messages.
194+
*/
195+
data class MessagingStyle (
196+
val user: Person,
197+
val conversationTitle: String? = null,
198+
val messages: List<MessagingStyleMessage?>,
199+
val isGroupConversation: Boolean
200+
201+
) {
202+
companion object {
203+
@Suppress("LocalVariableName")
204+
fun fromList(__pigeon_list: List<Any?>): MessagingStyle {
205+
val user = __pigeon_list[0] as Person
206+
val conversationTitle = __pigeon_list[1] as String?
207+
val messages = __pigeon_list[2] as List<MessagingStyleMessage?>
208+
val isGroupConversation = __pigeon_list[3] as Boolean
209+
return MessagingStyle(user, conversationTitle, messages, isGroupConversation)
210+
}
211+
}
212+
fun toList(): List<Any?> {
213+
return listOf(
214+
user,
215+
conversationTitle,
216+
messages,
217+
isGroupConversation,
218+
)
219+
}
220+
}
110221
private object NotificationsPigeonCodec : StandardMessageCodec() {
111222
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
112223
return when (type) {
@@ -120,6 +231,21 @@ private object NotificationsPigeonCodec : StandardMessageCodec() {
120231
InboxStyle.fromList(it)
121232
}
122233
}
234+
131.toByte() -> {
235+
return (readValue(buffer) as? List<Any?>)?.let {
236+
Person.fromList(it)
237+
}
238+
}
239+
132.toByte() -> {
240+
return (readValue(buffer) as? List<Any?>)?.let {
241+
MessagingStyleMessage.fromList(it)
242+
}
243+
}
244+
133.toByte() -> {
245+
return (readValue(buffer) as? List<Any?>)?.let {
246+
MessagingStyle.fromList(it)
247+
}
248+
}
123249
else -> super.readValueOfType(type, buffer)
124250
}
125251
}
@@ -133,6 +259,18 @@ private object NotificationsPigeonCodec : StandardMessageCodec() {
133259
stream.write(130)
134260
writeValue(stream, value.toList())
135261
}
262+
is Person -> {
263+
stream.write(131)
264+
writeValue(stream, value.toList())
265+
}
266+
is MessagingStyleMessage -> {
267+
stream.write(132)
268+
writeValue(stream, value.toList())
269+
}
270+
is MessagingStyle -> {
271+
stream.write(133)
272+
writeValue(stream, value.toList())
273+
}
136274
else -> super.writeValue(stream, value)
137275
}
138276
}
@@ -159,7 +297,23 @@ interface AndroidNotificationHostApi {
159297
* https://developer.android.com/reference/kotlin/android/app/NotificationManager.html#notify
160298
* https://developer.android.com/reference/androidx/core/app/NotificationCompat.Builder
161299
*/
162-
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?)
300+
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?)
301+
/**
302+
* Wraps `androidx.core.app.NotificationManagerCompat.getActiveNotifications`,
303+
* combined with `androidx.core.app.NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification`.
304+
*
305+
* The `tag` is used to find a notification that matches the
306+
* same tag from the active notifications list.
307+
*
308+
* Returns null if active notifications list is empty or none
309+
* of the notification matches the `tag`, else returns messaging
310+
* style information of the matching active notification.
311+
*
312+
* See:
313+
* https://developer.android.com/reference/kotlin/androidx/core/app/NotificationManagerCompat#getActiveNotifications()
314+
* https://developer.android.com/reference/kotlin/androidx/core/app/NotificationCompat.MessagingStyle#extractMessagingStyleFromNotification(android.app.Notification)
315+
*/
316+
fun getActiveNotificationMessagingStyleByTag(tag: String): MessagingStyle?
163317

164318
companion object {
165319
/** The codec used by AndroidNotificationHostApi. */
@@ -187,9 +341,11 @@ interface AndroidNotificationHostApi {
187341
val groupKeyArg = args[9] as String?
188342
val inboxStyleArg = args[10] as InboxStyle?
189343
val isGroupSummaryArg = args[11] as Boolean?
190-
val smallIconResourceNameArg = args[12] as String?
344+
val messagingStyleArg = args[12] as MessagingStyle?
345+
val numberArg = args[13].let { num -> if (num is Int) num.toLong() else num as Long? }
346+
val smallIconResourceNameArg = args[14] as String?
191347
val wrapped: List<Any?> = try {
192-
api.notify(tagArg, idArg, autoCancelArg, channelIdArg, colorArg, contentIntentArg, contentTextArg, contentTitleArg, extrasArg, groupKeyArg, inboxStyleArg, isGroupSummaryArg, smallIconResourceNameArg)
348+
api.notify(tagArg, idArg, autoCancelArg, channelIdArg, colorArg, contentIntentArg, contentTextArg, contentTitleArg, extrasArg, groupKeyArg, inboxStyleArg, isGroupSummaryArg, messagingStyleArg, numberArg, smallIconResourceNameArg)
193349
listOf(null)
194350
} catch (exception: Throwable) {
195351
wrapError(exception)
@@ -200,6 +356,23 @@ interface AndroidNotificationHostApi {
200356
channel.setMessageHandler(null)
201357
}
202358
}
359+
run {
360+
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.zulip.AndroidNotificationHostApi.getActiveNotificationMessagingStyleByTag$separatedMessageChannelSuffix", codec)
361+
if (api != null) {
362+
channel.setMessageHandler { message, reply ->
363+
val args = message as List<Any?>
364+
val tagArg = args[0] as String
365+
val wrapped: List<Any?> = try {
366+
listOf(api.getActiveNotificationMessagingStyleByTag(tagArg))
367+
} catch (exception: Throwable) {
368+
wrapError(exception)
369+
}
370+
reply.reply(wrapped)
371+
}
372+
} else {
373+
channel.setMessageHandler(null)
374+
}
375+
}
203376
}
204377
}
205378
}

android/app/src/main/kotlin/com/zulip/flutter/ZulipPlugin.kt

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,38 @@ import android.util.Log
88
import androidx.annotation.Keep
99
import androidx.core.app.NotificationCompat
1010
import androidx.core.app.NotificationManagerCompat
11+
import androidx.core.graphics.drawable.IconCompat
1112
import io.flutter.embedding.engine.plugins.FlutterPlugin
1213

1314
private const val TAG = "ZulipPlugin"
1415

16+
fun toAndroidPerson(person: Person): androidx.core.app.Person {
17+
return androidx.core.app.Person.Builder().apply {
18+
person.iconBitmap?.let { setIcon(IconCompat.createWithData(it, 0, it.size)) }
19+
setKey(person.key)
20+
setName(person.name)
21+
}.build()
22+
}
23+
24+
fun toPigeonPerson(person: androidx.core.app.Person): Person {
25+
return Person(
26+
// The API doesn't provide a way to retrieve the icon data,
27+
// so we set this to null.
28+
//
29+
// Notably, Android retains a limited number [1] of messages
30+
// in the messaging style, and it also retains the icon data
31+
// for persons within those messages. Therefore, there's no
32+
// need to include the person's icon data in each message.
33+
// Only one icon data instance is needed for each unique
34+
// person's key in the retained messages.
35+
//
36+
// [1]: https://developer.android.com/reference/androidx/core/app/NotificationCompat.MessagingStyle#MAXIMUM_RETAINED_MESSAGES()
37+
null,
38+
person.key!!,
39+
person.name!!.toString(),
40+
)
41+
}
42+
1543
private class AndroidNotificationHost(val context: Context)
1644
: AndroidNotificationHostApi {
1745
@SuppressLint(
@@ -33,6 +61,8 @@ private class AndroidNotificationHost(val context: Context)
3361
groupKey: String?,
3462
inboxStyle: InboxStyle?,
3563
isGroupSummary: Boolean?,
64+
messagingStyle: MessagingStyle?,
65+
number: Long?,
3666
smallIconResourceName: String?
3767
) {
3868
val notification = NotificationCompat.Builder(context, channelId).apply {
@@ -60,11 +90,48 @@ private class AndroidNotificationHost(val context: Context)
6090
.setSummaryText(it.summaryText)
6191
) }
6292
isGroupSummary?.let { setGroupSummary(it) }
93+
messagingStyle?.let { messagingStyle ->
94+
val style = NotificationCompat.MessagingStyle(toAndroidPerson(messagingStyle.user))
95+
.setConversationTitle(messagingStyle.conversationTitle)
96+
.setGroupConversation(messagingStyle.isGroupConversation)
97+
messagingStyle.messages?.forEach { it?.let {
98+
style.addMessage(NotificationCompat.MessagingStyle.Message(
99+
it.text,
100+
it.timestampMs,
101+
toAndroidPerson(it.person),
102+
))
103+
} }
104+
setStyle(style)
105+
}
106+
number?.let { setNumber(it.toInt()) }
63107
smallIconResourceName?.let { setSmallIcon(context.resources.getIdentifier(
64108
it, "drawable", context.packageName)) }
65109
}.build()
66110
NotificationManagerCompat.from(context).notify(tag, id.toInt(), notification)
67111
}
112+
113+
override fun getActiveNotificationMessagingStyleByTag(tag: String): MessagingStyle? {
114+
val activeNotification = NotificationManagerCompat.from(context)
115+
.activeNotifications
116+
.find { it.tag == tag }
117+
activeNotification?.notification?.let { notification ->
118+
NotificationCompat.MessagingStyle
119+
.extractMessagingStyleFromNotification(notification)
120+
?.let { style ->
121+
return MessagingStyle(
122+
toPigeonPerson(style.user),
123+
style.conversationTitle!!.toString(),
124+
style.messages.map { MessagingStyleMessage(
125+
it.text!!.toString(),
126+
it.timestamp,
127+
toPigeonPerson(it.person!!)
128+
) },
129+
style.isGroupConversation,
130+
)
131+
}
132+
}
133+
return null
134+
}
68135
}
69136

70137
/** A Flutter plugin for the Zulip app's ad-hoc needs. */

0 commit comments

Comments
 (0)