Skip to content

Commit b48cc90

Browse files
use UNNotificationRequest to schedule local notification for iOS 10 and above (#5667)
1 parent 68b0a23 commit b48cc90

File tree

4 files changed

+174
-36
lines changed

4 files changed

+174
-36
lines changed

FirebaseMessaging/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# unreleased
2+
- [changed] Use UNNotificationRequest to schedule local notification for local timezone notification for iOS 10 and above. This should also fix the issue that '%' was not properly shown in title and body. (#5667)
3+
14
# 2020-05 -- v4.4.1
25
- [changed] Updated NSError with a failure reason to give more details on the error. (#5511)
36

FirebaseMessaging/Sources/FIRMMessageCode.h

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,13 @@ typedef NS_ENUM(NSInteger, FIRMessagingMessageCode) {
9090
kFIRMessagingMessageCodeConnection022 = 5022, // I-FCM005022
9191
kFIRMessagingMessageCodeConnection023 = 5023, // I-FCM005023
9292
// FIRMessagingContextManagerService.m
93-
kFIRMessagingMessageCodeContextManagerService000 = 6000, // I-FCM006000
94-
kFIRMessagingMessageCodeContextManagerService001 = 6001, // I-FCM006001
95-
kFIRMessagingMessageCodeContextManagerService002 = 6002, // I-FCM006002
96-
kFIRMessagingMessageCodeContextManagerService003 = 6003, // I-FCM006003
97-
kFIRMessagingMessageCodeContextManagerService004 = 6004, // I-FCM006004
98-
kFIRMessagingMessageCodeContextManagerService005 = 6005, // I-FCM006005
93+
kFIRMessagingMessageCodeContextManagerService000 = 6000, // I-FCM006000
94+
kFIRMessagingMessageCodeContextManagerService001 = 6001, // I-FCM006001
95+
kFIRMessagingMessageCodeContextManagerService002 = 6002, // I-FCM006002
96+
kFIRMessagingMessageCodeContextManagerService003 = 6003, // I-FCM006003
97+
kFIRMessagingMessageCodeContextManagerService004 = 6004, // I-FCM006004
98+
kFIRMessagingMessageCodeContextManagerService005 = 6005, // I-FCM006005
99+
kFIRMessagingMessageCodeContextManagerServiceFailedLocalSchedule = 6006, // I-FCM006006
99100
// FIRMessagingDataMessageManager.m
100101
// DO NOT USE 7005
101102
kFIRMessagingMessageCodeDataMessageManager000 = 7000, // I-FCM007000

FirebaseMessaging/Sources/FIRMessagingContextManagerService.m

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16+
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 || \
17+
__MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_10_14 || __TV_OS_VERSION_MAX_ALLOWED >= __TV_10_0 || \
18+
__WATCH_OS_VERSION_MAX_ALLOWED >= __WATCHOS_3_0 || TARGET_OS_MACCATALYST
19+
#import <UserNotifications/UserNotifications.h>
20+
#endif
1621

1722
#import "FirebaseMessaging/Sources/FIRMessagingContextManagerService.h"
1823

@@ -22,6 +27,7 @@
2227

2328
#import <GoogleUtilities/GULAppDelegateSwizzler.h>
2429

30+
#define kFIRMessagingContextManagerPrefix @"gcm."
2531
#define kFIRMessagingContextManagerPrefixKey @"google.c.cm."
2632
#define kFIRMessagingContextManagerNotificationKeyPrefix @"gcm.notification."
2733

@@ -50,6 +56,7 @@
5056
kFIRMessagingContextManagerNotificationKeyPrefix @"sound";
5157
NSString *const kFIRMessagingContextManagerContentAvailableKey =
5258
kFIRMessagingContextManagerNotificationKeyPrefix @"content-available";
59+
static NSString *const kFIRMessagingID = kFIRMessagingContextManagerPrefix @"message_id";
5360
static NSString *const kFIRMessagingAPNSPayloadKey = @"aps";
5461

5562
typedef NS_ENUM(NSUInteger, FIRMessagingContextManagerMessageType) {
@@ -129,7 +136,69 @@ + (BOOL)handleContextManagerLocalTimeMessage:(NSDictionary *)message {
129136
return YES;
130137
}
131138

139+
+ (void)scheduleiOS10LocalNotificationForMessage:(NSDictionary *)message atDate:(NSDate *)date {
140+
NSCalendar *calendar = [NSCalendar currentCalendar];
141+
if (@available(macOS 10.14, iOS 10.0, watchOS 3.0, tvOS 10.0, *)) {
142+
NSCalendarUnit unit = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay |
143+
NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond;
144+
NSDateComponents *dateComponents = [calendar components:(NSCalendarUnit)unit fromDate:date];
145+
UNCalendarNotificationTrigger *trigger =
146+
[UNCalendarNotificationTrigger triggerWithDateMatchingComponents:dateComponents repeats:NO];
147+
148+
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
149+
NSDictionary *apsDictionary = message;
150+
151+
// Badge is universal
152+
if (apsDictionary[kFIRMessagingContextManagerBadgeKey]) {
153+
content.badge = apsDictionary[kFIRMessagingContextManagerBadgeKey];
154+
}
155+
#if TARGET_OS_IOS || TARGET_OS_OSX || TARGET_OS_WATCH
156+
// The following fields are not available on tvOS
157+
if ([apsDictionary[kFIRMessagingContextManagerBodyKey] length]) {
158+
content.body = apsDictionary[kFIRMessagingContextManagerBodyKey];
159+
}
160+
if ([apsDictionary[kFIRMessagingContextManagerTitleKey] length]) {
161+
content.title = apsDictionary[kFIRMessagingContextManagerTitleKey];
162+
}
163+
164+
if (apsDictionary[kFIRMessagingContextManagerSoundKey]) {
165+
content.sound = apsDictionary[kFIRMessagingContextManagerSoundKey];
166+
}
167+
168+
if (apsDictionary[kFIRMessagingContextManagerCategoryKey]) {
169+
content.categoryIdentifier = apsDictionary[kFIRMessagingContextManagerCategoryKey];
170+
}
171+
172+
NSDictionary *userInfo = [self parseDataFromMessage:message];
173+
if (userInfo.count) {
174+
content.userInfo = userInfo;
175+
}
176+
#endif
177+
NSString *identifier = apsDictionary[kFIRMessagingID];
178+
if (!identifier) {
179+
identifier = [NSUUID UUID].UUIDString;
180+
}
181+
182+
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier
183+
content:content
184+
trigger:trigger];
185+
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
186+
[center addNotificationRequest:request
187+
withCompletionHandler:^(NSError *_Nullable error) {
188+
if (error) {
189+
FIRMessagingLoggerError(
190+
kFIRMessagingMessageCodeContextManagerServiceFailedLocalSchedule,
191+
@"Failed scheduling local timezone notification: %@.", error);
192+
}
193+
}];
194+
}
195+
}
196+
132197
+ (void)scheduleLocalNotificationForMessage:(NSDictionary *)message atDate:(NSDate *)date {
198+
if (@available(macOS 10.14, iOS 10.0, watchOS 3.0, tvOS 10.0, *)) {
199+
[self scheduleiOS10LocalNotificationForMessage:message atDate:date];
200+
return;
201+
}
133202
#if TARGET_OS_IOS
134203
NSDictionary *apsDictionary = message;
135204
#pragma clang diagnostic push

FirebaseMessaging/Tests/UnitTests/FIRMessagingContextManagerServiceTest.m

Lines changed: 95 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,30 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
16+
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 || \
17+
__MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_10_14 || __TV_OS_VERSION_MAX_ALLOWED >= __TV_10_0 || \
18+
__WATCH_OS_VERSION_MAX_ALLOWED >= __WATCHOS_3_0 || TARGET_OS_MACCATALYST
19+
#import <UserNotifications/UserNotifications.h>
20+
#endif
1721
#import <OCMock/OCMock.h>
1822
#import <XCTest/XCTest.h>
1923

2024
#import "FirebaseMessaging/Sources/FIRMessagingContextManagerService.h"
2125

26+
static NSString *const kBody = @"Save 20% off!";
27+
static NSString *const kUserInfoKey1 = @"level";
28+
static NSString *const kUserInfoKey2 = @"isPayUser";
29+
static NSString *const kUserInfoValue1 = @"5";
30+
static NSString *const kUserInfoValue2 = @"Yes";
31+
static NSString *const kMessageIdentifierKey = @"gcm.message_id";
32+
static NSString *const kMessageIdentifierValue = @"1584748495200141";
33+
2234
@interface FIRMessagingContextManagerServiceTest : XCTestCase
2335

2436
@property(nonatomic, readwrite, strong) NSDateFormatter *dateFormatter;
2537
@property(nonatomic, readwrite, strong) NSMutableArray *scheduledLocalNotifications;
38+
@property(nonatomic, readwrite, strong)
39+
NSMutableArray<UNNotificationRequest *> *requests API_AVAILABLE(ios(10.0));
2640

2741
@end
2842

@@ -33,7 +47,11 @@ - (void)setUp {
3347
self.dateFormatter = [[NSDateFormatter alloc] init];
3448
self.dateFormatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
3549
[self.dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
36-
self.scheduledLocalNotifications = [NSMutableArray array];
50+
self.scheduledLocalNotifications = [[NSMutableArray alloc] init];
51+
if (@available(macOS 10.14, iOS 10.0, watchOS 3.0, tvOS 10.0, *)) {
52+
self.requests = [[NSMutableArray alloc] init];
53+
}
54+
3755
[self mockSchedulingLocalNotifications];
3856
}
3957

@@ -62,34 +80,44 @@ - (void)testValidContextManagerMessage {
6280
XCTAssertTrue([FIRMessagingContextManagerService isContextManagerMessage:message]);
6381
}
6482

65-
// TODO: Enable these tests. They fail because we cannot schedule local
66-
// notifications on OSX without permission. It's better to mock AppDelegate's
67-
// scheduleLocalNotification to mock scheduling behavior.
68-
6983
/**
7084
* Context Manager message with future start date should be successfully scheduled.
7185
*/
7286
- (void)testMessageWithFutureStartTime {
73-
#if TARGET_OS_IOS
74-
NSString *messageIdentifier = @"fcm-cm-test1";
7587
// way into the future
7688
NSString *startTimeString = [self.dateFormatter stringFromDate:[NSDate distantFuture]];
7789
NSDictionary *message = @{
7890
kFIRMessagingContextManagerLocalTimeStart : startTimeString,
79-
kFIRMessagingContextManagerBodyKey : @"Hello world!",
80-
@"id" : messageIdentifier,
81-
@"hello" : @"world"
91+
kFIRMessagingContextManagerBodyKey : kBody,
92+
kMessageIdentifierKey : kMessageIdentifierValue,
93+
kUserInfoKey1 : kUserInfoValue1,
94+
kUserInfoKey2 : kUserInfoValue2
8295
};
83-
8496
XCTAssertTrue([FIRMessagingContextManagerService handleContextManagerMessage:message]);
8597

98+
if (@available(macOS 10.14, iOS 10.0, watchOS 3.0, tvOS 10.0, *)) {
99+
XCTAssertEqual(self.requests.count, 1);
100+
UNNotificationRequest *request = self.requests.firstObject;
101+
XCTAssertEqualObjects(request.identifier, kMessageIdentifierValue);
102+
#if TARGET_OS_IOS || TARGET_OS_WATCH || TARGET_OS_OSX
103+
XCTAssertEqualObjects(request.content.body, kBody);
104+
XCTAssertEqualObjects(request.content.userInfo[kUserInfoKey1], kUserInfoValue1);
105+
XCTAssertEqualObjects(request.content.userInfo[kUserInfoKey2], kUserInfoValue2);
106+
#endif
107+
return;
108+
}
109+
110+
#if TARGET_OS_IOS
86111
XCTAssertEqual(self.scheduledLocalNotifications.count, 1);
87112
#pragma clang diagnostic push
88113
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
89-
UILocalNotification *notification = [self.scheduledLocalNotifications firstObject];
114+
UILocalNotification *notification = self.scheduledLocalNotifications.firstObject;
90115
#pragma clang diagnostic pop
91116
NSDate *date = [self.dateFormatter dateFromString:startTimeString];
92117
XCTAssertEqual([notification.fireDate compare:date], NSOrderedSame);
118+
XCTAssertEqualObjects(notification.alertBody, kBody);
119+
XCTAssertEqualObjects(notification.userInfo[kUserInfoKey1], kUserInfoValue1);
120+
XCTAssertEqualObjects(notification.userInfo[kUserInfoKey2], kUserInfoValue2);
93121
#endif
94122
}
95123

@@ -98,18 +126,21 @@ - (void)testMessageWithFutureStartTime {
98126
*/
99127
- (void)testMessageWithPastEndTime {
100128
#if TARGET_OS_IOS
101-
NSString *messageIdentifier = @"fcm-cm-test1";
102129
NSString *startTimeString = @"2010-01-12 12:00:00"; // way into the past
103130
NSString *endTimeString = @"2011-01-12 12:00:00"; // way into the past
104131
NSDictionary *message = @{
105132
kFIRMessagingContextManagerLocalTimeStart : startTimeString,
106133
kFIRMessagingContextManagerLocalTimeEnd : endTimeString,
107-
kFIRMessagingContextManagerBodyKey : @"Hello world!",
108-
@"id" : messageIdentifier,
134+
kFIRMessagingContextManagerBodyKey : kBody,
135+
kMessageIdentifierKey : kMessageIdentifierValue,
109136
@"hello" : @"world"
110137
};
111138

112139
XCTAssertTrue([FIRMessagingContextManagerService handleContextManagerMessage:message]);
140+
if (@available(macOS 10.14, iOS 10.0, watchOS 3.0, tvOS 10.0, *)) {
141+
XCTAssertEqual(self.requests.count, 0);
142+
return;
143+
}
113144
XCTAssertEqual(self.scheduledLocalNotifications.count, 0);
114145
#endif
115146
}
@@ -120,7 +151,6 @@ - (void)testMessageWithPastEndTime {
120151
*/
121152
- (void)testMessageWithPastStartAndFutureEndTime {
122153
#if TARGET_OS_IOS
123-
NSString *messageIdentifier = @"fcm-cm-test1";
124154
NSDate *startDate = [NSDate dateWithTimeIntervalSinceNow:-1000]; // past
125155
NSDate *endDate = [NSDate dateWithTimeIntervalSinceNow:1000]; // future
126156
NSString *startTimeString = [self.dateFormatter stringFromDate:startDate];
@@ -129,13 +159,23 @@ - (void)testMessageWithPastStartAndFutureEndTime {
129159
NSDictionary *message = @{
130160
kFIRMessagingContextManagerLocalTimeStart : startTimeString,
131161
kFIRMessagingContextManagerLocalTimeEnd : endTimeString,
132-
kFIRMessagingContextManagerBodyKey : @"Hello world!",
133-
@"id" : messageIdentifier,
134-
@"hello" : @"world"
162+
kFIRMessagingContextManagerBodyKey : kBody,
163+
kMessageIdentifierKey : kMessageIdentifierValue,
164+
kUserInfoKey1 : kUserInfoValue1,
165+
kUserInfoKey2 : kUserInfoValue2
135166
};
136167

137168
XCTAssertTrue([FIRMessagingContextManagerService handleContextManagerMessage:message]);
138169

170+
if (@available(macOS 10.14, iOS 10.0, watchOS 3.0, tvOS 10.0, *)) {
171+
XCTAssertEqual(self.requests.count, 1);
172+
UNNotificationRequest *request = self.requests.firstObject;
173+
XCTAssertEqualObjects(request.identifier, kMessageIdentifierValue);
174+
XCTAssertEqualObjects(request.content.body, kBody);
175+
XCTAssertEqualObjects(request.content.userInfo[kUserInfoKey1], kUserInfoValue1);
176+
XCTAssertEqualObjects(request.content.userInfo[kUserInfoKey2], kUserInfoValue2);
177+
return;
178+
}
139179
XCTAssertEqual(self.scheduledLocalNotifications.count, 1);
140180
#pragma clang diagnostic push
141181
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
@@ -145,6 +185,8 @@ - (void)testMessageWithPastStartAndFutureEndTime {
145185
XCTAssertEqual([notification.fireDate compare:startDate], NSOrderedDescending);
146186
// schedule notification after end date
147187
XCTAssertEqual([notification.fireDate compare:endDate], NSOrderedAscending);
188+
XCTAssertEqualObjects(notification.userInfo[kUserInfoKey1], kUserInfoValue1);
189+
XCTAssertEqualObjects(notification.userInfo[kUserInfoKey2], kUserInfoValue2);
148190
#endif
149191
}
150192

@@ -153,35 +195,58 @@ - (void)testMessageWithPastStartAndFutureEndTime {
153195
*/
154196
- (void)testTimedNotificationsUserInfo {
155197
#if TARGET_OS_IOS
156-
NSString *messageIdentifierKey = @"message.id";
157-
NSString *messageIdentifier = @"fcm-cm-test1";
158198
// way into the future
159199
NSString *startTimeString = [self.dateFormatter stringFromDate:[NSDate distantFuture]];
160200

161-
NSString *customDataKey = @"hello";
162-
NSString *customData = @"world";
163201
NSDictionary *message = @{
164202
kFIRMessagingContextManagerLocalTimeStart : startTimeString,
165-
kFIRMessagingContextManagerBodyKey : @"Hello world!",
166-
messageIdentifierKey : messageIdentifier,
167-
customDataKey : customData,
203+
kFIRMessagingContextManagerBodyKey : kBody,
204+
kMessageIdentifierKey : kMessageIdentifierValue,
205+
kUserInfoKey1 : kUserInfoValue1,
206+
kUserInfoKey2 : kUserInfoValue2
168207
};
169208

170209
XCTAssertTrue([FIRMessagingContextManagerService handleContextManagerMessage:message]);
171-
210+
if (@available(macOS 10.14, iOS 10.0, watchOS 3.0, tvOS 10.0, *)) {
211+
XCTAssertEqual(self.requests.count, 1);
212+
UNNotificationRequest *request = self.requests.firstObject;
213+
XCTAssertEqualObjects(request.identifier, kMessageIdentifierValue);
214+
XCTAssertEqualObjects(request.content.body, kBody);
215+
XCTAssertEqualObjects(request.content.userInfo[kUserInfoKey1], kUserInfoValue1);
216+
XCTAssertEqualObjects(request.content.userInfo[kUserInfoKey2], kUserInfoValue2);
217+
return;
218+
}
172219
XCTAssertEqual(self.scheduledLocalNotifications.count, 1);
173220
#pragma clang diagnostic push
174221
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
175222
UILocalNotification *notification = [self.scheduledLocalNotifications firstObject];
176223
#pragma clang diagnostic pop
177-
XCTAssertEqualObjects(notification.userInfo[messageIdentifierKey], messageIdentifier);
178-
XCTAssertEqualObjects(notification.userInfo[customDataKey], customData);
224+
XCTAssertEqualObjects(notification.userInfo[kUserInfoKey1], kUserInfoValue1);
225+
XCTAssertEqualObjects(notification.userInfo[kUserInfoKey2], kUserInfoValue2);
179226
#endif
180227
}
181228

182229
#pragma mark - Private Helpers
183230

184231
- (void)mockSchedulingLocalNotifications {
232+
if (@available(macOS 10.14, iOS 10.0, watchOS 3.0, tvOS 10.0, *)) {
233+
id mockNotificationCenter =
234+
OCMPartialMock([UNUserNotificationCenter currentNotificationCenter]);
235+
__block UNNotificationRequest *request;
236+
[[[mockNotificationCenter stub] andDo:^(NSInvocation *invocation) {
237+
[self.requests addObject:request];
238+
}] addNotificationRequest:[OCMArg checkWithBlock:^BOOL(id obj) {
239+
if ([obj isKindOfClass:[UNNotificationRequest class]]) {
240+
request = obj;
241+
[self.requests addObject:request];
242+
return YES;
243+
}
244+
return NO;
245+
}]
246+
withCompletionHandler:^(NSError *_Nullable error){
247+
}];
248+
return;
249+
}
185250
#if TARGET_OS_IOS
186251
id mockApplication = OCMPartialMock([UIApplication sharedApplication]);
187252
#pragma clang diagnostic push

0 commit comments

Comments
 (0)