Skip to content

Commit 5496eff

Browse files
authored
Don't swizzle missing and optional delegate methods
FCM's swizzling of the user notification center currently swizzles only one of the two optional delegate methods (userNotificationCenter:willPresentNotification:withCompletionHandler:), but not the other (userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:). The didReceiveNotificationResponse, if implemented by the delegate, is the sole receiver of all user action on a notification, including simply tapping on the notification itself. Prior to this change, if the developer had implemented didReceiveNotificationResponse, then FCM would not be able to collect this event for analytics. Additionally, I changed the logic in FIRMessagingRemoteNotificationsProxy to check whether these methods are actually implemented before swizzling them. It was always swizzling, which meant it was adding an implementation if the method didn't exist. This would confuse iOS into thinking the developer did implement these methods and NOT fall back to delivering the notifications to the application delegate. With this change, if the developer did not implement these methods, then FCM will not swizzle those methods. That keeps the behavior true to what the developer intended.
1 parent 641b7c3 commit 5496eff

File tree

3 files changed

+157
-38
lines changed

3 files changed

+157
-38
lines changed

Example/Messaging/App/iOS/NotificationsController.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,6 @@ class NotificationsController: NSObject {
121121
// MARK: - UNUserNotificationCenterDelegate
122122
@available(iOS 10.0, *)
123123
extension NotificationsController: UNUserNotificationCenterDelegate {
124-
125124
func userNotificationCenter(_ center: UNUserNotificationCenter,
126125
willPresent notification: UNNotification,
127126
withCompletionHandler completionHandler:
@@ -132,4 +131,11 @@ extension NotificationsController: UNUserNotificationCenterDelegate {
132131
print("\(jsonString)")
133132
completionHandler([.alert, .badge, .sound])
134133
}
134+
135+
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
136+
print("Received notification response")
137+
let jsonString = response.notification.request.content.userInfo.jsonString ?? "{}"
138+
print("\(jsonString)")
139+
completionHandler()
140+
}
135141
}

Example/Messaging/Tests/FIRMessagingRemoteNotificationsProxyTest.m

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ void FCM_swizzle_appDidReceiveRemoteNotificationWithHandler(
5353
void (^handler)(UIBackgroundFetchResult));
5454
void FCM_swizzle_willPresentNotificationWithHandler(
5555
id self, SEL _cmd, id center, id notification, void (^handler)(NSUInteger));
56+
void FCM_swizzle_didReceiveNotificationResponseWithHandler(
57+
id self, SEL _cmd, id center, id response, void (^handler)());
5658

5759
@end
5860

@@ -90,6 +92,7 @@ @implementation IncompleteUserNotificationCenterDelegate
9092
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
9193
@interface FakeUserNotificationCenterDelegate : NSObject <UNUserNotificationCenterDelegate>
9294
@property(nonatomic) BOOL willPresentWasCalled;
95+
@property(nonatomic) BOOL didReceiveResponseWasCalled;
9396
@end
9497
@implementation FakeUserNotificationCenterDelegate
9598
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
@@ -98,6 +101,9 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center
98101
completionHandler {
99102
self.willPresentWasCalled = YES;
100103
}
104+
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler {
105+
self.didReceiveResponseWasCalled = YES;
106+
}
101107
@end
102108
#endif // __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
103109

@@ -219,7 +225,7 @@ - (void)testSwizzlingUserNotificationsCenterDelegate {
219225
}
220226

221227
// Use a fake delegate that doesn't actually implement the needed delegate method.
222-
// Our swizzled method should still be called.
228+
// Our swizzled method should not be called.
223229

224230
- (void)testIncompleteUserNotificationCenterDelegateMethod {
225231
// Early exit if running on pre iOS 10
@@ -229,20 +235,17 @@ - (void)testIncompleteUserNotificationCenterDelegateMethod {
229235
IncompleteUserNotificationCenterDelegate *delegate =
230236
[[IncompleteUserNotificationCenterDelegate alloc] init];
231237
[self.mockProxy swizzleUserNotificationCenterDelegate:delegate];
232-
SEL selector = @selector(userNotificationCenter:willPresentNotification:withCompletionHandler:);
233-
XCTAssertTrue([delegate respondsToSelector:selector]);
234-
// Invoking delegate method should also invoke our swizzled method
235-
// The swizzled method uses the +sharedProxy, which should be
236-
// returning our mocked proxy.
237-
// Use non-nil, proper classes, otherwise our SDK bails out.
238-
[delegate userNotificationCenter:OCMClassMock([UNUserNotificationCenter class])
239-
willPresentNotification:[self generateMockNotification]
240-
withCompletionHandler:^(NSUInteger options) {}];
241-
// Verify our swizzled method was called
242-
OCMVerify(FCM_swizzle_willPresentNotificationWithHandler);
238+
// Because the incomplete delete does not implement either of the optional delegate methods, we
239+
// should swizzle nothing. If we had swizzled them, then respondsToSelector: would return YES
240+
// even though the delegate does not implement the methods.
241+
SEL willPresentSelector = @selector(userNotificationCenter:willPresentNotification:withCompletionHandler:);
242+
XCTAssertFalse([delegate respondsToSelector:willPresentSelector]);
243+
SEL didReceiveResponseSelector =
244+
@selector(userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:);
245+
XCTAssertFalse([delegate respondsToSelector:didReceiveResponseSelector]);
243246
}
244247

245-
// Use an object that does actually implement the needed method. Both should be called.
248+
// Use an object that does actually implement the optional methods. Both should be called.
246249
- (void)testSwizzledUserNotificationsCenterDelegate {
247250
// Early exit if running on pre iOS 10
248251
if (![UNNotification class]) {
@@ -261,6 +264,14 @@ - (void)testSwizzledUserNotificationsCenterDelegate {
261264
OCMVerify(FCM_swizzle_willPresentNotificationWithHandler);
262265
// Verify our original method was called
263266
XCTAssertTrue(delegate.willPresentWasCalled);
267+
268+
[delegate userNotificationCenter:OCMClassMock([UNUserNotificationCenter class])
269+
didReceiveNotificationResponse:[self generateMockNotificationResponse]
270+
withCompletionHandler:^{}];
271+
// Verify our swizzled method was called
272+
OCMVerify(FCM_swizzle_appDidReceiveRemoteNotificationWithHandler);
273+
// Verify our original method was called
274+
XCTAssertTrue(delegate.didReceiveResponseWasCalled);
264275
}
265276

266277
- (id)generateMockNotification {
@@ -274,6 +285,14 @@ - (id)generateMockNotification {
274285
return mockNotification;
275286
}
276287

288+
- (id)generateMockNotificationResponse {
289+
// Stub out: response.[mock notification above]
290+
id mockNotificationResponse = OCMClassMock([UNNotificationResponse class]);
291+
id mockNotification = [self generateMockNotification];
292+
OCMStub([mockNotificationResponse notification]).andReturn(mockNotification);
293+
return mockNotificationResponse;
294+
}
295+
277296
#endif // __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
278297

279298
@end

Firebase/Messaging/FIRMessagingRemoteNotificationsProxy.m

Lines changed: 118 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828

2929
static NSString *kUserNotificationWillPresentSelectorString =
3030
@"userNotificationCenter:willPresentNotification:withCompletionHandler:";
31+
static NSString *kUserNotificationDidReceiveResponseSelectorString =
32+
@"userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:";
3133

3234
@interface FIRMessagingRemoteNotificationsProxy ()
3335

@@ -219,10 +221,26 @@ - (void)swizzleUserNotificationCenterDelegate:(id)delegate {
219221
if ([delegate conformsToProtocol:userNotificationCenterProtocol]) {
220222
SEL willPresentNotificationSelector =
221223
NSSelectorFromString(kUserNotificationWillPresentSelectorString);
222-
[self swizzleSelector:willPresentNotificationSelector
223-
inClass:[delegate class]
224-
withImplementation:(IMP)FCM_swizzle_willPresentNotificationWithHandler
225-
inProtocol:userNotificationCenterProtocol];
224+
// Swizzle the optional method
225+
// "userNotificationCenter:willPresentNotification:withCompletionHandler:", if it is
226+
// implemented. Do not swizzle otherwise, as an implementation *will* be created, which will
227+
// fool iOS into thinking that this method is implemented, and therefore not send notifications
228+
// to the fallback method in the app delegate
229+
// "application:didReceiveRemoteNotification:fetchCompletionHandler:".
230+
if ([delegate respondsToSelector:willPresentNotificationSelector]) {
231+
[self swizzleSelector:willPresentNotificationSelector
232+
inClass:[delegate class]
233+
withImplementation:(IMP)FCM_swizzle_willPresentNotificationWithHandler
234+
inProtocol:userNotificationCenterProtocol];
235+
}
236+
SEL didReceiveNotificationResponseSelector =
237+
NSSelectorFromString(kUserNotificationDidReceiveResponseSelectorString);
238+
if ([delegate respondsToSelector:didReceiveNotificationResponseSelector]) {
239+
[self swizzleSelector:didReceiveNotificationResponseSelector
240+
inClass:[delegate class]
241+
withImplementation:(IMP)FCM_swizzle_didReceiveNotificationResponseWithHandler
242+
inProtocol:userNotificationCenterProtocol];
243+
}
226244
self.currentUserNotificationCenterDelegate = delegate;
227245
self.hasSwizzledUserNotificationDelegate = YES;
228246
}
@@ -235,6 +253,7 @@ - (void)unswizzleUserNotificationCenterDelegate:(id)delegate {
235253
}
236254
SEL willPresentNotificationSelector =
237255
NSSelectorFromString(kUserNotificationWillPresentSelectorString);
256+
// Call unswizzle methods, even if the method was not implemented (it will fail gracefully).
238257
[self unswizzleSelector:willPresentNotificationSelector
239258
inClass:[self.currentUserNotificationCenterDelegate class]];
240259
self.currentUserNotificationCenterDelegate = nil;
@@ -526,25 +545,106 @@ void FCM_swizzle_willPresentNotificationWithHandler(
526545
return;
527546
}
528547

529-
// Valid original method signature, go ahead to swizzle.
548+
// Attempt to access the user info
549+
id notificationUserInfo = userInfoFromNotification(notification);
550+
551+
if (!notificationUserInfo) {
552+
// Could not access notification.request.content.userInfo.
553+
callOriginalMethodIfAvailable();
554+
return;
555+
}
556+
557+
[[FIRMessaging messaging] appDidReceiveMessage:notificationUserInfo];
558+
// Execute the original implementation.
559+
callOriginalMethodIfAvailable();
560+
}
561+
562+
/**
563+
* Swizzle the notification handler for iOS 10+ devices.
564+
* Signature of original handler is as below:
565+
* - (void)userNotificationCenter:(UNUserNotificationCenter *)center
566+
* didReceiveNotificationResponse:(UNNotificationResponse *)response
567+
* withCompletionHandler:(void (^)(void))completionHandler
568+
* In order to make FCM SDK compile and compatible with iOS SDKs before iOS 10, hide the
569+
* parameter types from the swizzling implementation.
570+
*/
571+
void FCM_swizzle_didReceiveNotificationResponseWithHandler(
572+
id self, SEL _cmd, id center, id response, void (^handler)()) {
573+
574+
FIRMessagingRemoteNotificationsProxy *proxy = [FIRMessagingRemoteNotificationsProxy sharedProxy];
575+
IMP original_imp = [proxy originalImplementationForSelector:_cmd];
576+
577+
void (^callOriginalMethodIfAvailable)() = ^{
578+
if (original_imp) {
579+
((void (*)(id, SEL, id, id, void (^)(void)))original_imp)(
580+
self, _cmd, center, response, handler);
581+
}
582+
return;
583+
};
584+
585+
Class notificationCenterClass = NSClassFromString(@"UNUserNotificationCenter");
586+
Class responseClass = NSClassFromString(@"UNNotificationResponse");
587+
if (!center || ![center isKindOfClass:[notificationCenterClass class]]) {
588+
// Invalid parameter type from the original method.
589+
// Do not swizzle, just execute the original method.
590+
callOriginalMethodIfAvailable();
591+
return;
592+
}
593+
594+
if (!response || ![response isKindOfClass:[responseClass class]]) {
595+
// Invalid parameter type from the original method.
596+
// Do not swizzle, just execute the original method.
597+
callOriginalMethodIfAvailable();
598+
return;
599+
}
600+
601+
if (!handler) {
602+
// Invalid parameter type from the original method.
603+
// Do not swizzle, just execute the original method.
604+
callOriginalMethodIfAvailable();
605+
return;
606+
}
607+
608+
// Try to access the response.notification property
609+
SEL notificationSelector = NSSelectorFromString(@"notification");
610+
if (![response respondsToSelector:notificationSelector]) {
611+
// Cannot access the .notification property.
612+
callOriginalMethodIfAvailable();
613+
return;
614+
}
615+
id notificationClass = NSClassFromString(@"UNNotification");
616+
id notification = getNamedPropertyFromObject(response, @"notification", notificationClass);
617+
618+
// With a notification object, use the common code to reach deep into notification
619+
// (notification.request.content.userInfo)
620+
id notificationUserInfo = userInfoFromNotification(notification);
621+
if (!notificationUserInfo) {
622+
// Could not access notification.request.content.userInfo.
623+
callOriginalMethodIfAvailable();
624+
return;
625+
}
626+
627+
[[FIRMessaging messaging] appDidReceiveMessage:notificationUserInfo];
628+
// Execute the original implementation.
629+
callOriginalMethodIfAvailable();
630+
}
631+
632+
id userInfoFromNotification(id notification) {
633+
530634
// Select the userInfo field from UNNotification.request.content.userInfo.
531635
SEL requestSelector = NSSelectorFromString(@"request");
532636
if (![notification respondsToSelector:requestSelector]) {
533-
// This is not the expected notification handler. Do not swizzle, just execute the original
534-
// method.
535-
callOriginalMethodIfAvailable();
536-
return;
637+
// Cannot access the request property.
638+
return nil;
537639
}
538640
Class requestClass = NSClassFromString(@"UNNotificationRequest");
539641
id notificationRequest = getNamedPropertyFromObject(notification, @"request", requestClass);
540642

541643
SEL notificationContentSelector = NSSelectorFromString(@"content");
542644
if (!notificationRequest
543645
|| ![notificationRequest respondsToSelector:notificationContentSelector]) {
544-
// This is not the expected notification handler. Do not swizzle, just execute the original
545-
// method.
546-
callOriginalMethodIfAvailable();
547-
return;
646+
// Cannot access the content property.
647+
return nil;
548648
}
549649
Class contentClass = NSClassFromString(@"UNNotificationContent");
550650
id notificationContent = getNamedPropertyFromObject(notificationRequest,
@@ -554,25 +654,19 @@ void FCM_swizzle_willPresentNotificationWithHandler(
554654
SEL notificationUserInfoSelector = NSSelectorFromString(@"userInfo");
555655
if (!notificationContent
556656
|| ![notificationContent respondsToSelector:notificationUserInfoSelector]) {
557-
// This is not the expected notification handler. Do not swizzle, just execute the original
558-
// method.
559-
callOriginalMethodIfAvailable();
560-
return;
657+
// Cannot access the userInfo property.
658+
return nil;
561659
}
562660
id notificationUserInfo = getNamedPropertyFromObject(notificationContent,
563661
@"userInfo",
564662
[NSDictionary class]);
565663

566664
if (!notificationUserInfo) {
567-
// This is not the expected notification handler. Do not swizzle, just execute the original
568-
// method.
569-
callOriginalMethodIfAvailable();
570-
return;
665+
// This is not the expected notification handler.
666+
return nil;
571667
}
572668

573-
[[FIRMessaging messaging] appDidReceiveMessage:notificationUserInfo];
574-
// Execute the original implementation.
575-
callOriginalMethodIfAvailable();
669+
return notificationUserInfo;
576670
}
577671

578672
void FCM_swizzle_applicationReceivedRemoteMessage(

0 commit comments

Comments
 (0)