Skip to content

Commit 54fb403

Browse files
notif android: Migrate to cross-platform Pigeon API for navigation
1 parent 7cda0d0 commit 54fb403

16 files changed

+540
-676
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,79 @@
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 = maybeIntentExtrasData(intent)
14+
val api = NotificationHostApiImpl(maybeNotifPayload?.let { NotificationDataFromLaunch(it) })
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 maybeExtrasData = maybeIntentExtrasData(intent)
25+
if (notificationTapEventListener != null && maybeExtrasData != null) {
26+
notificationTapEventListener!!.onNotificationTapEvent(NotificationTapEvent(payload = maybeExtrasData))
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 maybeIntentExtrasData(intent: Intent): Map<Any?, Any?>? {
41+
var extrasData: Map<Any?, Any?>? = 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+
extrasData =
48+
bundle.keySet().mapNotNull { key -> bundle.getString(key)?.let { key to it } }
49+
.toMap<Any?, Any?>()
50+
}
51+
}
52+
}
53+
return extrasData
54+
}
55+
}
56+
57+
private class NotificationHostApiImpl(val maybeDataFromLaunch: NotificationDataFromLaunch?) :
58+
NotificationHostApi {
59+
override fun getNotificationDataFromLaunch(): NotificationDataFromLaunch? {
60+
return maybeDataFromLaunch
61+
}
62+
}
63+
64+
private class NotificationTapEventListener : NotificationTapEventsStreamHandler() {
65+
private var eventSink: PigeonEventSink<NotificationTapEvent>? = null
66+
67+
override fun onListen(p0: Any?, sink: PigeonEventSink<NotificationTapEvent>) {
68+
eventSink = sink
69+
}
70+
71+
fun onNotificationTapEvent(data: NotificationTapEvent) {
72+
eventSink?.success(data)
73+
}
74+
75+
fun onEventsDone() {
76+
eventSink?.endOfStream()
77+
eventSink = null
78+
}
679
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
// Autogenerated from Pigeon (v25.0.0), 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+
/** Generated class from Pigeon that represents data sent in messages. */
38+
data class NotificationDataFromLaunch (
39+
/**
40+
* The raw payload that is attached to the notification,
41+
* holding the information required to carry out the navigation.
42+
*
43+
* See [NotificationHostApi.getNotificationDataFromLaunch].
44+
*/
45+
val payload: Map<Any?, Any?>
46+
)
47+
{
48+
companion object {
49+
fun fromList(pigeonVar_list: List<Any?>): NotificationDataFromLaunch {
50+
val payload = pigeonVar_list[0] as Map<Any?, Any?>
51+
return NotificationDataFromLaunch(payload)
52+
}
53+
}
54+
fun toList(): List<Any?> {
55+
return listOf(
56+
payload,
57+
)
58+
}
59+
}
60+
61+
/** Generated class from Pigeon that represents data sent in messages. */
62+
data class NotificationTapEvent (
63+
/**
64+
* The raw payload that is attached to the notification,
65+
* holding the information required to carry out the navigation.
66+
*
67+
* See [notificationTapEvents].
68+
*/
69+
val payload: Map<Any?, Any?>
70+
)
71+
{
72+
companion object {
73+
fun fromList(pigeonVar_list: List<Any?>): NotificationTapEvent {
74+
val payload = pigeonVar_list[0] as Map<Any?, Any?>
75+
return NotificationTapEvent(payload)
76+
}
77+
}
78+
fun toList(): List<Any?> {
79+
return listOf(
80+
payload,
81+
)
82+
}
83+
}
84+
private open class NotificationsPigeonCodec : StandardMessageCodec() {
85+
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
86+
return when (type) {
87+
129.toByte() -> {
88+
return (readValue(buffer) as? List<Any?>)?.let {
89+
NotificationDataFromLaunch.fromList(it)
90+
}
91+
}
92+
130.toByte() -> {
93+
return (readValue(buffer) as? List<Any?>)?.let {
94+
NotificationTapEvent.fromList(it)
95+
}
96+
}
97+
else -> super.readValueOfType(type, buffer)
98+
}
99+
}
100+
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
101+
when (value) {
102+
is NotificationDataFromLaunch -> {
103+
stream.write(129)
104+
writeValue(stream, value.toList())
105+
}
106+
is NotificationTapEvent -> {
107+
stream.write(130)
108+
writeValue(stream, value.toList())
109+
}
110+
else -> super.writeValue(stream, value)
111+
}
112+
}
113+
}
114+
115+
val NotificationsPigeonMethodCodec = StandardMethodCodec(NotificationsPigeonCodec());
116+
117+
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
118+
interface NotificationHostApi {
119+
/**
120+
* Retrieves notification data if the app was launched by tapping on a notification.
121+
*
122+
* On iOS, this returns `launchOptions.remoteNotification`,
123+
* which is the raw APNs data dictionary
124+
* if the app launch was opened by a notification tap,
125+
* else null. See Apple doc:
126+
* https://developer.apple.com/documentation/uikit/uiapplication/launchoptionskey/remotenotification
127+
*
128+
* On Android, this checks if the launch `intent` has the intent data uri
129+
* starting with `zulip://notification` and has the extras bundle containing
130+
* the notification open payload we set during creating the notification.
131+
* Either returns the payload we set in the extras bundle, or null if the
132+
* `intent` doesn't match the preconditions, meaning launch wasn't triggered
133+
* by a notification.
134+
*/
135+
fun getNotificationDataFromLaunch(): NotificationDataFromLaunch?
136+
137+
companion object {
138+
/** The codec used by NotificationHostApi. */
139+
val codec: MessageCodec<Any?> by lazy {
140+
NotificationsPigeonCodec()
141+
}
142+
/** Sets up an instance of `NotificationHostApi` to handle messages through the `binaryMessenger`. */
143+
@JvmOverloads
144+
fun setUp(binaryMessenger: BinaryMessenger, api: NotificationHostApi?, messageChannelSuffix: String = "") {
145+
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
146+
run {
147+
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.zulip.NotificationHostApi.getNotificationDataFromLaunch$separatedMessageChannelSuffix", codec)
148+
if (api != null) {
149+
channel.setMessageHandler { _, reply ->
150+
val wrapped: List<Any?> = try {
151+
listOf(api.getNotificationDataFromLaunch())
152+
} catch (exception: Throwable) {
153+
wrapError(exception)
154+
}
155+
reply.reply(wrapped)
156+
}
157+
} else {
158+
channel.setMessageHandler(null)
159+
}
160+
}
161+
}
162+
}
163+
}
164+
165+
private class NotificationsPigeonStreamHandler<T>(
166+
val wrapper: NotificationsPigeonEventChannelWrapper<T>
167+
) : EventChannel.StreamHandler {
168+
var pigeonSink: PigeonEventSink<T>? = null
169+
170+
override fun onListen(p0: Any?, sink: EventChannel.EventSink) {
171+
pigeonSink = PigeonEventSink<T>(sink)
172+
wrapper.onListen(p0, pigeonSink!!)
173+
}
174+
175+
override fun onCancel(p0: Any?) {
176+
pigeonSink = null
177+
wrapper.onCancel(p0)
178+
}
179+
}
180+
181+
interface NotificationsPigeonEventChannelWrapper<T> {
182+
open fun onListen(p0: Any?, sink: PigeonEventSink<T>) {}
183+
184+
open fun onCancel(p0: Any?) {}
185+
}
186+
187+
class PigeonEventSink<T>(private val sink: EventChannel.EventSink) {
188+
fun success(value: T) {
189+
sink.success(value)
190+
}
191+
192+
fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
193+
sink.error(errorCode, errorMessage, errorDetails)
194+
}
195+
196+
fun endOfStream() {
197+
sink.endOfStream()
198+
}
199+
}
200+
201+
abstract class NotificationTapEventsStreamHandler : NotificationsPigeonEventChannelWrapper<NotificationTapEvent> {
202+
companion object {
203+
fun register(messenger: BinaryMessenger, streamHandler: NotificationTapEventsStreamHandler, instanceName: String = "") {
204+
var channelName: String = "dev.flutter.pigeon.zulip.NotificationEventChannelApi.notificationTapEvents"
205+
if (instanceName.isNotEmpty()) {
206+
channelName += ".$instanceName"
207+
}
208+
val internalStreamHandler = NotificationsPigeonStreamHandler<NotificationTapEvent>(streamHandler)
209+
EventChannel(messenger, channelName, NotificationsPigeonMethodCodec).setStreamHandler(internalStreamHandler)
210+
}
211+
}
212+
}
213+

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

+16-2
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,18 @@ var notificationsPigeonMethodCodec = FlutterStandardMethodCodec(readerWriter: No
159159
protocol NotificationHostApi {
160160
/// Retrieves notification data if the app was launched by tapping on a notification.
161161
///
162-
/// Returns `launchOptions.remoteNotification`,
162+
/// On iOS, this returns `launchOptions.remoteNotification`,
163163
/// which is the raw APNs data dictionary
164164
/// if the app launch was opened by a notification tap,
165165
/// else null. See Apple doc:
166166
/// https://developer.apple.com/documentation/uikit/uiapplication/launchoptionskey/remotenotification
167+
///
168+
/// On Android, this checks if the launch `intent` has the intent data uri
169+
/// starting with `zulip://notification` and has the extras bundle containing
170+
/// the notification open payload we set during creating the notification.
171+
/// Either returns the payload we set in the extras bundle, or null if the
172+
/// `intent` doesn't match the preconditions, meaning launch wasn't triggered
173+
/// by a notification.
167174
func getNotificationDataFromLaunch() throws -> NotificationDataFromLaunch?
168175
}
169176

@@ -175,11 +182,18 @@ class NotificationHostApiSetup {
175182
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
176183
/// Retrieves notification data if the app was launched by tapping on a notification.
177184
///
178-
/// Returns `launchOptions.remoteNotification`,
185+
/// On iOS, this returns `launchOptions.remoteNotification`,
179186
/// which is the raw APNs data dictionary
180187
/// if the app launch was opened by a notification tap,
181188
/// else null. See Apple doc:
182189
/// https://developer.apple.com/documentation/uikit/uiapplication/launchoptionskey/remotenotification
190+
///
191+
/// On Android, this checks if the launch `intent` has the intent data uri
192+
/// starting with `zulip://notification` and has the extras bundle containing
193+
/// the notification open payload we set during creating the notification.
194+
/// Either returns the payload we set in the extras bundle, or null if the
195+
/// `intent` doesn't match the preconditions, meaning launch wasn't triggered
196+
/// by a notification.
183197
let getNotificationDataFromLaunchChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.zulip.NotificationHostApi.getNotificationDataFromLaunch\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
184198
if let api = api {
185199
getNotificationDataFromLaunchChannel.setMessageHandler { _, reply in

0 commit comments

Comments
 (0)