Skip to content

Commit 8232b39

Browse files
notif android: Migrate to cross-platform Pigeon API for navigation
1 parent 2884c11 commit 8232b39

File tree

14 files changed

+614
-660
lines changed

14 files changed

+614
-660
lines changed

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

+5-2
Original file line numberDiff line numberDiff line change
@@ -104,22 +104,25 @@ data class AndroidIntent (
104104
val action: String,
105105
val dataUrl: String,
106106
/** A combination of flags from [IntentFlag]. */
107-
val flags: Long
107+
val flags: Long,
108+
val extrasData: Map<String, String>
108109
)
109110
{
110111
companion object {
111112
fun fromList(pigeonVar_list: List<Any?>): AndroidIntent {
112113
val action = pigeonVar_list[0] as String
113114
val dataUrl = pigeonVar_list[1] as String
114115
val flags = pigeonVar_list[2] as Long
115-
return AndroidIntent(action, dataUrl, flags)
116+
val extrasData = pigeonVar_list[3] as Map<String, String>
117+
return AndroidIntent(action, dataUrl, flags, extrasData)
116118
}
117119
}
118120
fun toList(): List<Any?> {
119121
return listOf(
120122
action,
121123
dataUrl,
122124
flags,
125+
extrasData,
123126
)
124127
}
125128
}
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,80 @@
11
package com.zulip.flutter
22

3+
import android.content.Intent
34
import io.flutter.embedding.android.FlutterActivity
5+
import io.flutter.embedding.engine.FlutterEngine
46

5-
class MainActivity: FlutterActivity() {
7+
class MainActivity : FlutterActivity() {
8+
private var notificationTapEventListener: NotificationTapEventListener? = null
9+
10+
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
11+
super.configureFlutterEngine(flutterEngine)
12+
13+
val maybeNotifPayload = maybeIntentToNotificationPayload(intent)
14+
val api = NotificationHostApiImpl(maybeNotifPayload)
15+
NotificationHostApi.setUp(flutterEngine.dartExecutor.binaryMessenger, api)
16+
17+
notificationTapEventListener = NotificationTapEventListener()
18+
NotificationTapEventsStreamHandler.register(
19+
flutterEngine.dartExecutor.binaryMessenger, notificationTapEventListener!!
20+
)
21+
}
22+
23+
override fun onNewIntent(intent: Intent) {
24+
val maybeNotifData = maybeIntentToNotificationPayload(intent)
25+
if (notificationTapEventListener != null && maybeNotifData != null) {
26+
notificationTapEventListener!!.onNotificationTapEvent(maybeNotifData)
27+
return
28+
}
29+
30+
super.onNewIntent(intent)
31+
}
32+
33+
override fun cleanUpFlutterEngine(flutterEngine: FlutterEngine) {
34+
notificationTapEventListener?.onEventsDone()
35+
notificationTapEventListener = null
36+
37+
super.cleanUpFlutterEngine(flutterEngine)
38+
}
39+
40+
private fun maybeIntentToNotificationPayload(intent: Intent): NotificationPayloadForOpen? {
41+
var notifData: NotificationPayloadForOpen? = null
42+
if (intent.action == Intent.ACTION_VIEW) {
43+
val intentUrl = intent.data
44+
if (intentUrl?.scheme == "zulip" && intentUrl.authority == "notification") {
45+
val bundle = intent.getBundleExtra("data")
46+
if (bundle != null) {
47+
val payload =
48+
bundle.keySet().mapNotNull { key -> bundle.getString(key)?.let { key to it } }
49+
.toMap<Any?, Any?>()
50+
notifData = NotificationPayloadForOpen(payload)
51+
}
52+
}
53+
}
54+
return notifData
55+
}
56+
}
57+
58+
private class NotificationHostApiImpl(val maybeNotifPayload: NotificationPayloadForOpen?) :
59+
NotificationHostApi {
60+
override fun getNotificationDataFromLaunch(): NotificationPayloadForOpen? {
61+
return maybeNotifPayload
62+
}
63+
}
64+
65+
private class NotificationTapEventListener : NotificationTapEventsStreamHandler() {
66+
private var eventSink: PigeonEventSink<NotificationPayloadForOpen>? = null
67+
68+
override fun onListen(p0: Any?, sink: PigeonEventSink<NotificationPayloadForOpen>) {
69+
eventSink = sink
70+
}
71+
72+
fun onNotificationTapEvent(data: NotificationPayloadForOpen) {
73+
eventSink?.success(data)
74+
}
75+
76+
fun onEventsDone() {
77+
eventSink?.endOfStream()
78+
eventSink = null
79+
}
680
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// Autogenerated from Pigeon (v24.2.1), do not edit directly.
2+
// See also: https://pub.dev/packages/pigeon
3+
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
4+
5+
package com.zulip.flutter
6+
7+
import android.util.Log
8+
import io.flutter.plugin.common.BasicMessageChannel
9+
import io.flutter.plugin.common.BinaryMessenger
10+
import io.flutter.plugin.common.EventChannel
11+
import io.flutter.plugin.common.MessageCodec
12+
import io.flutter.plugin.common.StandardMethodCodec
13+
import io.flutter.plugin.common.StandardMessageCodec
14+
import java.io.ByteArrayOutputStream
15+
import java.nio.ByteBuffer
16+
17+
private fun wrapResult(result: Any?): List<Any?> {
18+
return listOf(result)
19+
}
20+
21+
private fun wrapError(exception: Throwable): List<Any?> {
22+
return if (exception is FlutterError) {
23+
listOf(
24+
exception.code,
25+
exception.message,
26+
exception.details
27+
)
28+
} else {
29+
listOf(
30+
exception.javaClass.simpleName,
31+
exception.toString(),
32+
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
33+
)
34+
}
35+
}
36+
37+
/**
38+
* The payload that is attached to each notification and holds
39+
* the information required to carry out the navigation.
40+
*
41+
* On iOS, the notification payload will be the APNs data from
42+
* the server.
43+
*
44+
* On Android, the payload will be the Intent extras bundle provided
45+
* during the creation of the notification.
46+
*
47+
* Generated class from Pigeon that represents data sent in messages.
48+
*/
49+
data class NotificationPayloadForOpen (
50+
val payload: Map<Any?, Any?>
51+
)
52+
{
53+
companion object {
54+
fun fromList(pigeonVar_list: List<Any?>): NotificationPayloadForOpen {
55+
val payload = pigeonVar_list[0] as Map<Any?, Any?>
56+
return NotificationPayloadForOpen(payload)
57+
}
58+
}
59+
fun toList(): List<Any?> {
60+
return listOf(
61+
payload,
62+
)
63+
}
64+
}
65+
private open class NotificationsPigeonCodec : StandardMessageCodec() {
66+
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
67+
return when (type) {
68+
129.toByte() -> {
69+
return (readValue(buffer) as? List<Any?>)?.let {
70+
NotificationPayloadForOpen.fromList(it)
71+
}
72+
}
73+
else -> super.readValueOfType(type, buffer)
74+
}
75+
}
76+
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
77+
when (value) {
78+
is NotificationPayloadForOpen -> {
79+
stream.write(129)
80+
writeValue(stream, value.toList())
81+
}
82+
else -> super.writeValue(stream, value)
83+
}
84+
}
85+
}
86+
87+
val NotificationsPigeonMethodCodec = StandardMethodCodec(NotificationsPigeonCodec());
88+
89+
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
90+
interface NotificationHostApi {
91+
/** Retrieves notification data if the app was launched by tapping on a notification. */
92+
fun getNotificationDataFromLaunch(): NotificationPayloadForOpen?
93+
94+
companion object {
95+
/** The codec used by NotificationHostApi. */
96+
val codec: MessageCodec<Any?> by lazy {
97+
NotificationsPigeonCodec()
98+
}
99+
/** Sets up an instance of `NotificationHostApi` to handle messages through the `binaryMessenger`. */
100+
@JvmOverloads
101+
fun setUp(binaryMessenger: BinaryMessenger, api: NotificationHostApi?, messageChannelSuffix: String = "") {
102+
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
103+
run {
104+
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.zulip.NotificationHostApi.getNotificationDataFromLaunch$separatedMessageChannelSuffix", codec)
105+
if (api != null) {
106+
channel.setMessageHandler { _, reply ->
107+
val wrapped: List<Any?> = try {
108+
listOf(api.getNotificationDataFromLaunch())
109+
} catch (exception: Throwable) {
110+
wrapError(exception)
111+
}
112+
reply.reply(wrapped)
113+
}
114+
} else {
115+
channel.setMessageHandler(null)
116+
}
117+
}
118+
}
119+
}
120+
}
121+
122+
private class NotificationsPigeonStreamHandler<T>(
123+
val wrapper: NotificationsPigeonEventChannelWrapper<T>
124+
) : EventChannel.StreamHandler {
125+
var pigeonSink: PigeonEventSink<T>? = null
126+
127+
override fun onListen(p0: Any?, sink: EventChannel.EventSink) {
128+
pigeonSink = PigeonEventSink<T>(sink)
129+
wrapper.onListen(p0, pigeonSink!!)
130+
}
131+
132+
override fun onCancel(p0: Any?) {
133+
pigeonSink = null
134+
wrapper.onCancel(p0)
135+
}
136+
}
137+
138+
interface NotificationsPigeonEventChannelWrapper<T> {
139+
open fun onListen(p0: Any?, sink: PigeonEventSink<T>) {}
140+
141+
open fun onCancel(p0: Any?) {}
142+
}
143+
144+
class PigeonEventSink<T>(private val sink: EventChannel.EventSink) {
145+
fun success(value: T) {
146+
sink.success(value)
147+
}
148+
149+
fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
150+
sink.error(errorCode, errorMessage, errorDetails)
151+
}
152+
153+
fun endOfStream() {
154+
sink.endOfStream()
155+
}
156+
}
157+
158+
abstract class NotificationTapEventsStreamHandler : NotificationsPigeonEventChannelWrapper<NotificationPayloadForOpen> {
159+
companion object {
160+
fun register(messenger: BinaryMessenger, streamHandler: NotificationTapEventsStreamHandler, instanceName: String = "") {
161+
var channelName: String = "dev.flutter.pigeon.zulip.NotificationHostEvents.notificationTapEvents"
162+
if (instanceName.isNotEmpty()) {
163+
channelName += ".$instanceName"
164+
}
165+
val internalStreamHandler = NotificationsPigeonStreamHandler<NotificationPayloadForOpen>(streamHandler)
166+
EventChannel(messenger, channelName, NotificationsPigeonMethodCodec).setStreamHandler(internalStreamHandler)
167+
}
168+
}
169+
}
170+

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

+5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import androidx.core.app.NotificationChannelCompat
1818
import androidx.core.app.NotificationCompat
1919
import androidx.core.app.NotificationManagerCompat
2020
import androidx.core.graphics.drawable.IconCompat
21+
import androidx.core.os.bundleOf
2122
import io.flutter.embedding.engine.plugins.FlutterPlugin
2223

2324
private const val TAG = "ZulipPlugin"
@@ -204,6 +205,10 @@ private class AndroidNotificationHost(val context: Context)
204205
MainActivity::class.java
205206
).apply {
206207
flags = intent.flags.toInt()
208+
putExtra(
209+
"data",
210+
bundleOf(*intent.extrasData.toList().toTypedArray())
211+
)
207212
} },
208213
it.flags.toInt())
209214
) }

ios/Runner/Notifications.g.swift

+3
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ private func nilOrValue<T>(_ value: Any?) -> T? {
7070
/// On iOS, the notification payload will be the APNs data from
7171
/// the server.
7272
///
73+
/// On Android, the payload will be the Intent extras bundle provided
74+
/// during the creation of the notification.
75+
///
7376
/// Generated class from Pigeon that represents data sent in messages.
7477
struct NotificationPayloadForOpen {
7578
var payload: [AnyHashable?: Any?]

lib/host/android_notifications.g.dart

+5
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ class AndroidIntent {
7878
required this.action,
7979
required this.dataUrl,
8080
this.flags = 0,
81+
required this.extrasData,
8182
});
8283

8384
String action;
@@ -87,11 +88,14 @@ class AndroidIntent {
8788
/// A combination of flags from [IntentFlag].
8889
int flags;
8990

91+
Map<String, String> extrasData;
92+
9093
Object encode() {
9194
return <Object?>[
9295
action,
9396
dataUrl,
9497
flags,
98+
extrasData,
9599
];
96100
}
97101

@@ -101,6 +105,7 @@ class AndroidIntent {
101105
action: result[0]! as String,
102106
dataUrl: result[1]! as String,
103107
flags: result[2]! as int,
108+
extrasData: (result[3] as Map<Object?, Object?>?)!.cast<String, String>(),
104109
);
105110
}
106111
}

lib/host/notifications.g.dart

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ PlatformException _createConnectionError(String channelName) {
2020
///
2121
/// On iOS, the notification payload will be the APNs data from
2222
/// the server.
23+
///
24+
/// On Android, the payload will be the Intent extras bundle provided
25+
/// during the creation of the notification.
2326
class NotificationPayloadForOpen {
2427
NotificationPayloadForOpen({
2528
required this.payload,

0 commit comments

Comments
 (0)