Skip to content

Commit 28ea77f

Browse files
notif android: Migrate to cross-platform Pigeon API for navigation
1 parent 4b2ade0 commit 28ea77f

15 files changed

+507
-677
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,180 @@
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+
* Generated class from Pigeon that represents data sent in messages.
42+
*/
43+
data class NotificationPayloadForOpen (
44+
val payload: Map<Any?, Any?>
45+
)
46+
{
47+
companion object {
48+
fun fromList(pigeonVar_list: List<Any?>): NotificationPayloadForOpen {
49+
val payload = pigeonVar_list[0] as Map<Any?, Any?>
50+
return NotificationPayloadForOpen(payload)
51+
}
52+
}
53+
fun toList(): List<Any?> {
54+
return listOf(
55+
payload,
56+
)
57+
}
58+
}
59+
private open class NotificationsPigeonCodec : StandardMessageCodec() {
60+
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
61+
return when (type) {
62+
129.toByte() -> {
63+
return (readValue(buffer) as? List<Any?>)?.let {
64+
NotificationPayloadForOpen.fromList(it)
65+
}
66+
}
67+
else -> super.readValueOfType(type, buffer)
68+
}
69+
}
70+
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
71+
when (value) {
72+
is NotificationPayloadForOpen -> {
73+
stream.write(129)
74+
writeValue(stream, value.toList())
75+
}
76+
else -> super.writeValue(stream, value)
77+
}
78+
}
79+
}
80+
81+
val NotificationsPigeonMethodCodec = StandardMethodCodec(NotificationsPigeonCodec());
82+
83+
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
84+
interface NotificationHostApi {
85+
/**
86+
* Retrieves notification data if the app was launched by tapping on a notification.
87+
*
88+
* On iOS, this checks and returns value for the `remoteNotification` key
89+
* in the `launchOptions` map. The value could be either the raw APNs data
90+
* dictionary, if the launch of the app was triggered by a notification tap,
91+
* otherwise it will be null.
92+
*
93+
* See: https://developer.apple.com/documentation/uikit/uiapplication/launchoptionskey/remotenotification
94+
*
95+
* On Android, this checks if the launch `intent` has the intent data uri
96+
* starting with `zulip://notification` and has the extras bundle containing
97+
* the notification open payload we set during creating the notification.
98+
* Either returns the payload we set in the extras bundle, or null if the
99+
* `intent` doesn't match the preconditions, meaning launch wasn't triggered
100+
* by a notification.
101+
*/
102+
fun getNotificationDataFromLaunch(): NotificationPayloadForOpen?
103+
104+
companion object {
105+
/** The codec used by NotificationHostApi. */
106+
val codec: MessageCodec<Any?> by lazy {
107+
NotificationsPigeonCodec()
108+
}
109+
/** Sets up an instance of `NotificationHostApi` to handle messages through the `binaryMessenger`. */
110+
@JvmOverloads
111+
fun setUp(binaryMessenger: BinaryMessenger, api: NotificationHostApi?, messageChannelSuffix: String = "") {
112+
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
113+
run {
114+
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.zulip.NotificationHostApi.getNotificationDataFromLaunch$separatedMessageChannelSuffix", codec)
115+
if (api != null) {
116+
channel.setMessageHandler { _, reply ->
117+
val wrapped: List<Any?> = try {
118+
listOf(api.getNotificationDataFromLaunch())
119+
} catch (exception: Throwable) {
120+
wrapError(exception)
121+
}
122+
reply.reply(wrapped)
123+
}
124+
} else {
125+
channel.setMessageHandler(null)
126+
}
127+
}
128+
}
129+
}
130+
}
131+
132+
private class NotificationsPigeonStreamHandler<T>(
133+
val wrapper: NotificationsPigeonEventChannelWrapper<T>
134+
) : EventChannel.StreamHandler {
135+
var pigeonSink: PigeonEventSink<T>? = null
136+
137+
override fun onListen(p0: Any?, sink: EventChannel.EventSink) {
138+
pigeonSink = PigeonEventSink<T>(sink)
139+
wrapper.onListen(p0, pigeonSink!!)
140+
}
141+
142+
override fun onCancel(p0: Any?) {
143+
pigeonSink = null
144+
wrapper.onCancel(p0)
145+
}
146+
}
147+
148+
interface NotificationsPigeonEventChannelWrapper<T> {
149+
open fun onListen(p0: Any?, sink: PigeonEventSink<T>) {}
150+
151+
open fun onCancel(p0: Any?) {}
152+
}
153+
154+
class PigeonEventSink<T>(private val sink: EventChannel.EventSink) {
155+
fun success(value: T) {
156+
sink.success(value)
157+
}
158+
159+
fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
160+
sink.error(errorCode, errorMessage, errorDetails)
161+
}
162+
163+
fun endOfStream() {
164+
sink.endOfStream()
165+
}
166+
}
167+
168+
abstract class NotificationTapEventsStreamHandler : NotificationsPigeonEventChannelWrapper<NotificationPayloadForOpen> {
169+
companion object {
170+
fun register(messenger: BinaryMessenger, streamHandler: NotificationTapEventsStreamHandler, instanceName: String = "") {
171+
var channelName: String = "dev.flutter.pigeon.zulip.NotificationHostEvents.notificationTapEvents"
172+
if (instanceName.isNotEmpty()) {
173+
channelName += ".$instanceName"
174+
}
175+
val internalStreamHandler = NotificationsPigeonStreamHandler<NotificationPayloadForOpen>(streamHandler)
176+
EventChannel(messenger, channelName, NotificationsPigeonMethodCodec).setStreamHandler(internalStreamHandler)
177+
}
178+
}
179+
}
180+

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

+14
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,13 @@ protocol NotificationHostApi {
135135
/// otherwise it will be null.
136136
///
137137
/// See: https://developer.apple.com/documentation/uikit/uiapplication/launchoptionskey/remotenotification
138+
///
139+
/// On Android, this checks if the launch `intent` has the intent data uri
140+
/// starting with `zulip://notification` and has the extras bundle containing
141+
/// the notification open payload we set during creating the notification.
142+
/// Either returns the payload we set in the extras bundle, or null if the
143+
/// `intent` doesn't match the preconditions, meaning launch wasn't triggered
144+
/// by a notification.
138145
func getNotificationDataFromLaunch() throws -> NotificationPayloadForOpen?
139146
}
140147

@@ -152,6 +159,13 @@ class NotificationHostApiSetup {
152159
/// otherwise it will be null.
153160
///
154161
/// See: https://developer.apple.com/documentation/uikit/uiapplication/launchoptionskey/remotenotification
162+
///
163+
/// On Android, this checks if the launch `intent` has the intent data uri
164+
/// starting with `zulip://notification` and has the extras bundle containing
165+
/// the notification open payload we set during creating the notification.
166+
/// Either returns the payload we set in the extras bundle, or null if the
167+
/// `intent` doesn't match the preconditions, meaning launch wasn't triggered
168+
/// by a notification.
155169
let getNotificationDataFromLaunchChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.zulip.NotificationHostApi.getNotificationDataFromLaunch\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
156170
if let api = api {
157171
getNotificationDataFromLaunchChannel.setMessageHandler { _, reply in

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

+7
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ class NotificationHostApi {
8888
/// otherwise it will be null.
8989
///
9090
/// See: https://developer.apple.com/documentation/uikit/uiapplication/launchoptionskey/remotenotification
91+
///
92+
/// On Android, this checks if the launch `intent` has the intent data uri
93+
/// starting with `zulip://notification` and has the extras bundle containing
94+
/// the notification open payload we set during creating the notification.
95+
/// Either returns the payload we set in the extras bundle, or null if the
96+
/// `intent` doesn't match the preconditions, meaning launch wasn't triggered
97+
/// by a notification.
9198
Future<NotificationPayloadForOpen?> getNotificationDataFromLaunch() async {
9299
final String pigeonVar_channelName = 'dev.flutter.pigeon.zulip.NotificationHostApi.getNotificationDataFromLaunch$pigeonVar_messageChannelSuffix';
93100
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(

0 commit comments

Comments
 (0)