Skip to content

Commit bd0306b

Browse files
notif ios: Buffer notification tap events; same as on Android
It turns out that `userNotificationCenter(_:didReceive:withCompletionHandler:)` does execute when app is terminated and later opened by tapping on a notification, so there is no need to check `launchOptions` in `application(_:didFinishLaunchingWithOptions:)` to handle this case. But because we currently handle both cases, there is a potential bug where if both are handled, it could cause double navigations for the same notification. This is not observed in practice, probably because we do not buffer events from `userNotificationCenter(_:didReceive:withCompletionHandler:)` currently and those events may be missed until Flutter init is complete and the handler (in NotificationOpenService.init) is setup. So, remove the unnecessary handlers for `launchOptions`. This also unifies our notification opening implementation between Android and iOS, where we handle all types of notification tap events via the same Pigeon EventChannel API.
1 parent 8a7e94a commit bd0306b

12 files changed

Lines changed: 82 additions & 550 deletions

File tree

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

Lines changed: 3 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,6 @@ import io.flutter.plugin.common.StandardMessageCodec
1414
import java.io.ByteArrayOutputStream
1515
import java.nio.ByteBuffer
1616
private object NotificationsPigeonUtils {
17-
18-
fun wrapResult(result: Any?): List<Any?> {
19-
return listOf(result)
20-
}
21-
22-
fun wrapError(exception: Throwable): List<Any?> {
23-
return if (exception is FlutterError) {
24-
listOf(
25-
exception.code,
26-
exception.message,
27-
exception.details
28-
)
29-
} else {
30-
listOf(
31-
exception.javaClass.simpleName,
32-
exception.toString(),
33-
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
34-
)
35-
}
36-
}
3717
fun deepEquals(a: Any?, b: Any?): Boolean {
3818
if (a is ByteArray && b is ByteArray) {
3919
return a.contentEquals(b)
@@ -78,40 +58,6 @@ class FlutterError (
7858
val details: Any? = null
7959
) : Throwable()
8060

81-
/** Generated class from Pigeon that represents data sent in messages. */
82-
data class NotificationDataFromLaunch (
83-
/**
84-
* The raw payload that is attached to the notification,
85-
* holding the information required to carry out the navigation.
86-
*
87-
* See [NotificationHostApi.getNotificationDataFromLaunch].
88-
*/
89-
val payload: Map<Any?, Any?>
90-
)
91-
{
92-
companion object {
93-
fun fromList(pigeonVar_list: List<Any?>): NotificationDataFromLaunch {
94-
val payload = pigeonVar_list[0] as Map<Any?, Any?>
95-
return NotificationDataFromLaunch(payload)
96-
}
97-
}
98-
fun toList(): List<Any?> {
99-
return listOf(
100-
payload,
101-
)
102-
}
103-
override fun equals(other: Any?): Boolean {
104-
if (other !is NotificationDataFromLaunch) {
105-
return false
106-
}
107-
if (this === other) {
108-
return true
109-
}
110-
return NotificationsPigeonUtils.deepEquals(toList(), other.toList()) }
111-
112-
override fun hashCode(): Int = toList().hashCode()
113-
}
114-
11561
/**
11662
* Generated class from Pigeon that represents data sent in messages.
11763
* This class should not be extended by any user class outside of the generated file.
@@ -202,16 +148,11 @@ private open class NotificationsPigeonCodec : StandardMessageCodec() {
202148
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
203149
return when (type) {
204150
129.toByte() -> {
205-
return (readValue(buffer) as? List<Any?>)?.let {
206-
NotificationDataFromLaunch.fromList(it)
207-
}
208-
}
209-
130.toByte() -> {
210151
return (readValue(buffer) as? List<Any?>)?.let {
211152
IosNotificationTapEvent.fromList(it)
212153
}
213154
}
214-
131.toByte() -> {
155+
130.toByte() -> {
215156
return (readValue(buffer) as? List<Any?>)?.let {
216157
AndroidNotificationTapEvent.fromList(it)
217158
}
@@ -221,16 +162,12 @@ private open class NotificationsPigeonCodec : StandardMessageCodec() {
221162
}
222163
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
223164
when (value) {
224-
is NotificationDataFromLaunch -> {
225-
stream.write(129)
226-
writeValue(stream, value.toList())
227-
}
228165
is IosNotificationTapEvent -> {
229-
stream.write(130)
166+
stream.write(129)
230167
writeValue(stream, value.toList())
231168
}
232169
is AndroidNotificationTapEvent -> {
233-
stream.write(131)
170+
stream.write(130)
234171
writeValue(stream, value.toList())
235172
}
236173
else -> super.writeValue(stream, value)
@@ -240,46 +177,6 @@ private open class NotificationsPigeonCodec : StandardMessageCodec() {
240177

241178
val NotificationsPigeonMethodCodec = StandardMethodCodec(NotificationsPigeonCodec())
242179

243-
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
244-
interface NotificationHostApi {
245-
/**
246-
* Retrieves notification data if the app was launched by tapping on a notification.
247-
*
248-
* Returns `launchOptions.remoteNotification`,
249-
* which is the raw APNs data dictionary
250-
* if the app launch was opened by a notification tap,
251-
* else null. See Apple doc:
252-
* https://developer.apple.com/documentation/uikit/uiapplication/launchoptionskey/remotenotification
253-
*/
254-
fun getNotificationDataFromLaunch(): NotificationDataFromLaunch?
255-
256-
companion object {
257-
/** The codec used by NotificationHostApi. */
258-
val codec: MessageCodec<Any?> by lazy {
259-
NotificationsPigeonCodec()
260-
}
261-
/** Sets up an instance of `NotificationHostApi` to handle messages through the `binaryMessenger`. */
262-
@JvmOverloads
263-
fun setUp(binaryMessenger: BinaryMessenger, api: NotificationHostApi?, messageChannelSuffix: String = "") {
264-
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
265-
run {
266-
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.zulip.NotificationHostApi.getNotificationDataFromLaunch$separatedMessageChannelSuffix", codec)
267-
if (api != null) {
268-
channel.setMessageHandler { _, reply ->
269-
val wrapped: List<Any?> = try {
270-
listOf(api.getNotificationDataFromLaunch())
271-
} catch (exception: Throwable) {
272-
NotificationsPigeonUtils.wrapError(exception)
273-
}
274-
reply.reply(wrapped)
275-
}
276-
} else {
277-
channel.setMessageHandler(null)
278-
}
279-
}
280-
}
281-
}
282-
}
283180

284181
private class NotificationsPigeonStreamHandler<T>(
285182
val wrapper: NotificationsPigeonEventChannelWrapper<T>

ios/Runner/AppDelegate.swift

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,6 @@ import UIKit
2020

2121
IosNativeHostApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: IosNativeHostApiImpl())
2222

23-
// Retrieve the remote notification payload from launch options;
24-
// this will be null if the launch wasn't triggered by a notification.
25-
let notificationPayload =
26-
launchOptions?[.remoteNotification] as? [AnyHashable: Any]
27-
let api = NotificationHostApiImpl(
28-
notificationPayload.map { NotificationDataFromLaunch(payload: $0) }
29-
)
30-
NotificationHostApiSetup.setUp(
31-
binaryMessenger: controller.binaryMessenger,
32-
api: api
33-
)
34-
3523
notificationTapEventListener = NotificationTapEventListener()
3624
NotificationTapEventsStreamHandler.register(
3725
with: controller.binaryMessenger,
@@ -58,16 +46,3 @@ import UIKit
5846
completionHandler()
5947
}
6048
}
61-
62-
private class NotificationHostApiImpl: NotificationHostApi {
63-
private let maybeDataFromLaunch: NotificationDataFromLaunch?
64-
65-
init(_ maybeDataFromLaunch: NotificationDataFromLaunch?) {
66-
self.maybeDataFromLaunch = maybeDataFromLaunch
67-
}
68-
69-
func getNotificationDataFromLaunch() -> NotificationDataFromLaunch? {
70-
maybeDataFromLaunch
71-
}
72-
}
73-

ios/Runner/NotificationTapEventListener.swift

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,34 @@ import UIKit
55
// https://github.com/flutter/packages/blob/2dff6213a/packages/pigeon/example/app/ios/Runner/AppDelegate.swift#L49-L74
66
class NotificationTapEventListener: NotificationTapEventsStreamHandler {
77
var eventSink: PigeonEventSink<NotificationTapEvent>?
8+
var buffer: [NotificationTapEvent] = []
89

910
override func onListen(
1011
withArguments arguments: Any?,
1112
sink: PigeonEventSink<NotificationTapEvent>
1213
) {
1314
eventSink = sink
15+
if !buffer.isEmpty {
16+
buffer.forEach {
17+
sink.success($0)
18+
}
19+
buffer.removeAll()
20+
}
21+
}
22+
23+
override func onCancel(withArguments arguments: Any?) {
24+
if let eventSink = self.eventSink {
25+
eventSink.endOfStream()
26+
self.eventSink = nil
27+
}
1428
}
1529

1630
func onNotificationTapEvent(payload: [AnyHashable: Any]) {
17-
eventSink?.success(IosNotificationTapEvent(payload: payload))
31+
let event = IosNotificationTapEvent(payload: payload)
32+
if let eventSink = self.eventSink {
33+
eventSink.success(event)
34+
} else {
35+
buffer.append(event)
36+
}
1837
}
1938
}

ios/Runner/Notifications.g.swift

Lines changed: 3 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -29,32 +29,6 @@ final class PigeonError: Error {
2929
}
3030
}
3131

32-
private func wrapResult(_ result: Any?) -> [Any?] {
33-
return [result]
34-
}
35-
36-
private func wrapError(_ error: Any) -> [Any?] {
37-
if let pigeonError = error as? PigeonError {
38-
return [
39-
pigeonError.code,
40-
pigeonError.message,
41-
pigeonError.details,
42-
]
43-
}
44-
if let flutterError = error as? FlutterError {
45-
return [
46-
flutterError.code,
47-
flutterError.message,
48-
flutterError.details,
49-
]
50-
}
51-
return [
52-
"\(error)",
53-
"\(type(of: error))",
54-
"Stacktrace: \(Thread.callStackSymbols)",
55-
]
56-
}
57-
5832
private func isNullish(_ value: Any?) -> Bool {
5933
return value is NSNull || value == nil
6034
}
@@ -128,35 +102,6 @@ func deepHashNotifications(value: Any?, hasher: inout Hasher) {
128102

129103

130104

131-
/// Generated class from Pigeon that represents data sent in messages.
132-
struct NotificationDataFromLaunch: Hashable {
133-
/// The raw payload that is attached to the notification,
134-
/// holding the information required to carry out the navigation.
135-
///
136-
/// See [NotificationHostApi.getNotificationDataFromLaunch].
137-
var payload: [AnyHashable?: Any?]
138-
139-
140-
// swift-format-ignore: AlwaysUseLowerCamelCase
141-
static func fromList(_ pigeonVar_list: [Any?]) -> NotificationDataFromLaunch? {
142-
let payload = pigeonVar_list[0] as! [AnyHashable?: Any?]
143-
144-
return NotificationDataFromLaunch(
145-
payload: payload
146-
)
147-
}
148-
func toList() -> [Any?] {
149-
return [
150-
payload
151-
]
152-
}
153-
static func == (lhs: NotificationDataFromLaunch, rhs: NotificationDataFromLaunch) -> Bool {
154-
return deepEqualsNotifications(lhs.toList(), rhs.toList()) }
155-
func hash(into hasher: inout Hasher) {
156-
deepHashNotifications(value: toList(), hasher: &hasher)
157-
}
158-
}
159-
160105
/// Generated class from Pigeon that represents data sent in messages.
161106
/// This protocol should not be extended by any user class outside of the generated file.
162107
protocol NotificationTapEvent {
@@ -235,10 +180,8 @@ private class NotificationsPigeonCodecReader: FlutterStandardReader {
235180
override func readValue(ofType type: UInt8) -> Any? {
236181
switch type {
237182
case 129:
238-
return NotificationDataFromLaunch.fromList(self.readValue() as! [Any?])
239-
case 130:
240183
return IosNotificationTapEvent.fromList(self.readValue() as! [Any?])
241-
case 131:
184+
case 130:
242185
return AndroidNotificationTapEvent.fromList(self.readValue() as! [Any?])
243186
default:
244187
return super.readValue(ofType: type)
@@ -248,14 +191,11 @@ private class NotificationsPigeonCodecReader: FlutterStandardReader {
248191

249192
private class NotificationsPigeonCodecWriter: FlutterStandardWriter {
250193
override func writeValue(_ value: Any) {
251-
if let value = value as? NotificationDataFromLaunch {
194+
if let value = value as? IosNotificationTapEvent {
252195
super.writeByte(129)
253196
super.writeValue(value.toList())
254-
} else if let value = value as? IosNotificationTapEvent {
255-
super.writeByte(130)
256-
super.writeValue(value.toList())
257197
} else if let value = value as? AndroidNotificationTapEvent {
258-
super.writeByte(131)
198+
super.writeByte(130)
259199
super.writeValue(value.toList())
260200
} else {
261201
super.writeValue(value)
@@ -279,46 +219,6 @@ class NotificationsPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable
279219

280220
var notificationsPigeonMethodCodec = FlutterStandardMethodCodec(readerWriter: NotificationsPigeonCodecReaderWriter());
281221

282-
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
283-
protocol NotificationHostApi {
284-
/// Retrieves notification data if the app was launched by tapping on a notification.
285-
///
286-
/// Returns `launchOptions.remoteNotification`,
287-
/// which is the raw APNs data dictionary
288-
/// if the app launch was opened by a notification tap,
289-
/// else null. See Apple doc:
290-
/// https://developer.apple.com/documentation/uikit/uiapplication/launchoptionskey/remotenotification
291-
func getNotificationDataFromLaunch() throws -> NotificationDataFromLaunch?
292-
}
293-
294-
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
295-
class NotificationHostApiSetup {
296-
static var codec: FlutterStandardMessageCodec { NotificationsPigeonCodec.shared }
297-
/// Sets up an instance of `NotificationHostApi` to handle messages through the `binaryMessenger`.
298-
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: NotificationHostApi?, messageChannelSuffix: String = "") {
299-
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
300-
/// Retrieves notification data if the app was launched by tapping on a notification.
301-
///
302-
/// Returns `launchOptions.remoteNotification`,
303-
/// which is the raw APNs data dictionary
304-
/// if the app launch was opened by a notification tap,
305-
/// else null. See Apple doc:
306-
/// https://developer.apple.com/documentation/uikit/uiapplication/launchoptionskey/remotenotification
307-
let getNotificationDataFromLaunchChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.zulip.NotificationHostApi.getNotificationDataFromLaunch\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
308-
if let api = api {
309-
getNotificationDataFromLaunchChannel.setMessageHandler { _, reply in
310-
do {
311-
let result = try api.getNotificationDataFromLaunch()
312-
reply(wrapResult(result))
313-
} catch {
314-
reply(wrapError(error))
315-
}
316-
}
317-
} else {
318-
getNotificationDataFromLaunchChannel.setMessageHandler(nil)
319-
}
320-
}
321-
}
322222

323223
private class PigeonStreamHandler<ReturnType>: NSObject, FlutterStreamHandler {
324224
private let wrapper: PigeonEventChannelWrapper<ReturnType>

0 commit comments

Comments
 (0)