Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions FirebaseMessaging/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# 2020-06 -- v4.4.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)

# 2020-05 -- v4.4.1
- [changed] Updated NSError with a failure reason to give more details on the error. (#5511)

Expand Down
13 changes: 7 additions & 6 deletions FirebaseMessaging/Sources/FIRMMessageCode.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,13 @@ typedef NS_ENUM(NSInteger, FIRMessagingMessageCode) {
kFIRMessagingMessageCodeConnection022 = 5022, // I-FCM005022
kFIRMessagingMessageCodeConnection023 = 5023, // I-FCM005023
// FIRMessagingContextManagerService.m
kFIRMessagingMessageCodeContextManagerService000 = 6000, // I-FCM006000
kFIRMessagingMessageCodeContextManagerService001 = 6001, // I-FCM006001
kFIRMessagingMessageCodeContextManagerService002 = 6002, // I-FCM006002
kFIRMessagingMessageCodeContextManagerService003 = 6003, // I-FCM006003
kFIRMessagingMessageCodeContextManagerService004 = 6004, // I-FCM006004
kFIRMessagingMessageCodeContextManagerService005 = 6005, // I-FCM006005
kFIRMessagingMessageCodeContextManagerService000 = 6000, // I-FCM006000
kFIRMessagingMessageCodeContextManagerService001 = 6001, // I-FCM006001
kFIRMessagingMessageCodeContextManagerService002 = 6002, // I-FCM006002
kFIRMessagingMessageCodeContextManagerService003 = 6003, // I-FCM006003
kFIRMessagingMessageCodeContextManagerService004 = 6004, // I-FCM006004
kFIRMessagingMessageCodeContextManagerService005 = 6005, // I-FCM006005
kFIRMessagingMessageCodeContextManagerServiceFailedLocalSchedule = 6006, // I-FCM006006
// FIRMessagingDataMessageManager.m
// DO NOT USE 7005
kFIRMessagingMessageCodeDataMessageManager000 = 7000, // I-FCM007000
Expand Down
70 changes: 70 additions & 0 deletions FirebaseMessaging/Sources/FIRMessagingContextManagerService.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 || \
__MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_10_14 || __TV_OS_VERSION_MAX_ALLOWED >= __TV_10_0 || \
__WATCH_OS_VERSION_MAX_ALLOWED >= __WATCHOS_3_0 || TARGET_OS_MACCATALYST
#import <UserNotifications/UserNotifications.h>
#endif

#import "FirebaseMessaging/Sources/FIRMessagingContextManagerService.h"

Expand All @@ -22,6 +27,7 @@

#import <GoogleUtilities/GULAppDelegateSwizzler.h>

#define kFIRMessagingContextManagerPrefix @"gcm."
#define kFIRMessagingContextManagerPrefixKey @"google.c.cm."
#define kFIRMessagingContextManagerNotificationKeyPrefix @"gcm.notification."

Expand Down Expand Up @@ -50,6 +56,7 @@
kFIRMessagingContextManagerNotificationKeyPrefix @"sound";
NSString *const kFIRMessagingContextManagerContentAvailableKey =
kFIRMessagingContextManagerNotificationKeyPrefix @"content-available";
static NSString *const kFIRMessagingID = kFIRMessagingContextManagerPrefix @"message_id";
static NSString *const kFIRMessagingAPNSPayloadKey = @"aps";

typedef NS_ENUM(NSUInteger, FIRMessagingContextManagerMessageType) {
Expand Down Expand Up @@ -129,7 +136,70 @@ + (BOOL)handleContextManagerLocalTimeMessage:(NSDictionary *)message {
return YES;
}

+ (void)scheduleiOS10LocalNotificationForMessage:(NSDictionary *)message atDate:(NSDate *)date {
NSCalendar *calendar = [NSCalendar currentCalendar];
if (@available(macOS 10.14, iOS 10.0, watchOS 3.0, tvOS 10.0, *)) {
NSCalendarUnit unit = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay |
NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond;
NSDateComponents *dateComponents = [calendar components:(NSCalendarUnit)unit fromDate:date];
UNCalendarNotificationTrigger *trigger =
[UNCalendarNotificationTrigger triggerWithDateMatchingComponents:dateComponents
repeats:YES];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder why repeats is YES? It looks like it won't repeat any time soon with the specified calendar units (maybe in the next era maybe :) ).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I think this should be no as backend every day will send a new scheduled notification. And client has no way to set when to end, so this should be a backend control thing. I will double test again today and tomorrow.


UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
NSDictionary *apsDictionary = message;

// Badge is universal
if (apsDictionary[kFIRMessagingContextManagerBadgeKey]) {
content.badge = apsDictionary[kFIRMessagingContextManagerBadgeKey];
}
#if TARGET_OS_IOS || TARGET_OS_OSX || TARGET_OS_WATCH
// The following fields are not available on tvOS
if ([apsDictionary[kFIRMessagingContextManagerBodyKey] length]) {
content.body = apsDictionary[kFIRMessagingContextManagerBodyKey];
}
if ([apsDictionary[kFIRMessagingContextManagerTitleKey] length]) {
content.title = apsDictionary[kFIRMessagingContextManagerTitleKey];
}

if (apsDictionary[kFIRMessagingContextManagerSoundKey]) {
content.sound = apsDictionary[kFIRMessagingContextManagerSoundKey];
}

if (apsDictionary[kFIRMessagingContextManagerCategoryKey]) {
content.categoryIdentifier = apsDictionary[kFIRMessagingContextManagerCategoryKey];
}

NSDictionary *userInfo = [self parseDataFromMessage:message];
if (userInfo.count) {
content.userInfo = userInfo;
}
#endif
NSString *identifier = apsDictionary[kFIRMessagingID];
if (!identifier) {
identifier = [NSUUID UUID].UUIDString;
}

UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier
content:content
trigger:trigger];
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center addNotificationRequest:request
withCompletionHandler:^(NSError *_Nullable error) {
if (error) {
FIRMessagingLoggerError(
kFIRMessagingMessageCodeContextManagerServiceFailedLocalSchedule,
@"Failed scheduling local timezone notification: %@.", error);
}
}];
}
}

+ (void)scheduleLocalNotificationForMessage:(NSDictionary *)message atDate:(NSDate *)date {
if (@available(macOS 10.14, iOS 10.0, watchOS 3.0, tvOS 10.0, *)) {
[self scheduleiOS10LocalNotificationForMessage:message atDate:date];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we need to make any validation of the date (e.g. if it is in the past or in a distant future)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return;
}
#if TARGET_OS_IOS
NSDictionary *apsDictionary = message;
#pragma clang diagnostic push
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,30 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 || \
__MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_10_14 || __TV_OS_VERSION_MAX_ALLOWED >= __TV_10_0 || \
__WATCH_OS_VERSION_MAX_ALLOWED >= __WATCHOS_3_0 || TARGET_OS_MACCATALYST
#import <UserNotifications/UserNotifications.h>
#endif
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>

#import "FirebaseMessaging/Sources/FIRMessagingContextManagerService.h"

static NSString *const kBody = @"Save 20% off!";
static NSString *const kUserInfoKey1 = @"level";
static NSString *const kUserInfoKey2 = @"isPayUser";
static NSString *const kUserInfoValue1 = @"5";
static NSString *const kUserInfoValue2 = @"Yes";
static NSString *const kMessageIdentifierKey = @"gcm.message_id";
static NSString *const kMessageIdentifierValue = @"1584748495200141";

@interface FIRMessagingContextManagerServiceTest : XCTestCase

@property(nonatomic, readwrite, strong) NSDateFormatter *dateFormatter;
@property(nonatomic, readwrite, strong) NSMutableArray *scheduledLocalNotifications;
@property(nonatomic, readwrite, strong)
NSMutableArray<UNNotificationRequest *> *requests API_AVAILABLE(ios(10.0));

@end

Expand All @@ -33,7 +47,11 @@ - (void)setUp {
self.dateFormatter = [[NSDateFormatter alloc] init];
self.dateFormatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
[self.dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
self.scheduledLocalNotifications = [NSMutableArray array];
self.scheduledLocalNotifications = [[NSMutableArray alloc] init];
if (@available(macOS 10.14, iOS 10.0, watchOS 3.0, tvOS 10.0, *)) {
self.requests = [[NSMutableArray alloc] init];
}

[self mockSchedulingLocalNotifications];
}

Expand Down Expand Up @@ -62,34 +80,44 @@ - (void)testValidContextManagerMessage {
XCTAssertTrue([FIRMessagingContextManagerService isContextManagerMessage:message]);
}

// TODO: Enable these tests. They fail because we cannot schedule local
// notifications on OSX without permission. It's better to mock AppDelegate's
// scheduleLocalNotification to mock scheduling behavior.

/**
* Context Manager message with future start date should be successfully scheduled.
*/
- (void)testMessageWithFutureStartTime {
#if TARGET_OS_IOS
NSString *messageIdentifier = @"fcm-cm-test1";
// way into the future
NSString *startTimeString = [self.dateFormatter stringFromDate:[NSDate distantFuture]];
NSDictionary *message = @{
kFIRMessagingContextManagerLocalTimeStart : startTimeString,
kFIRMessagingContextManagerBodyKey : @"Hello world!",
@"id" : messageIdentifier,
@"hello" : @"world"
kFIRMessagingContextManagerBodyKey : kBody,
kMessageIdentifierKey : kMessageIdentifierValue,
kUserInfoKey1 : kUserInfoValue1,
kUserInfoKey2 : kUserInfoValue2
};

XCTAssertTrue([FIRMessagingContextManagerService handleContextManagerMessage:message]);

if (@available(macOS 10.14, iOS 10.0, watchOS 3.0, tvOS 10.0, *)) {
XCTAssertEqual(self.requests.count, 1);
UNNotificationRequest *request = self.requests.firstObject;
XCTAssertEqualObjects(request.identifier, kMessageIdentifierValue);
#if TARGET_OS_IOS || TARGET_OS_WATCH || TARGET_OS_OSX
XCTAssertEqualObjects(request.content.body, kBody);
XCTAssertEqualObjects(request.content.userInfo[kUserInfoKey1], kUserInfoValue1);
XCTAssertEqualObjects(request.content.userInfo[kUserInfoKey2], kUserInfoValue2);
#endif
return;
}

#if TARGET_OS_IOS
XCTAssertEqual(self.scheduledLocalNotifications.count, 1);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
UILocalNotification *notification = [self.scheduledLocalNotifications firstObject];
UILocalNotification *notification = self.scheduledLocalNotifications.firstObject;
#pragma clang diagnostic pop
NSDate *date = [self.dateFormatter dateFromString:startTimeString];
XCTAssertEqual([notification.fireDate compare:date], NSOrderedSame);
XCTAssertEqualObjects(notification.alertBody, kBody);
XCTAssertEqualObjects(notification.userInfo[kUserInfoKey1], kUserInfoValue1);
XCTAssertEqualObjects(notification.userInfo[kUserInfoKey2], kUserInfoValue2);
#endif
}

Expand All @@ -98,18 +126,21 @@ - (void)testMessageWithFutureStartTime {
*/
- (void)testMessageWithPastEndTime {
#if TARGET_OS_IOS
NSString *messageIdentifier = @"fcm-cm-test1";
NSString *startTimeString = @"2010-01-12 12:00:00"; // way into the past
NSString *endTimeString = @"2011-01-12 12:00:00"; // way into the past
NSDictionary *message = @{
kFIRMessagingContextManagerLocalTimeStart : startTimeString,
kFIRMessagingContextManagerLocalTimeEnd : endTimeString,
kFIRMessagingContextManagerBodyKey : @"Hello world!",
@"id" : messageIdentifier,
kFIRMessagingContextManagerBodyKey : kBody,
kMessageIdentifierKey : kMessageIdentifierValue,
@"hello" : @"world"
};

XCTAssertTrue([FIRMessagingContextManagerService handleContextManagerMessage:message]);
if (@available(macOS 10.14, iOS 10.0, watchOS 3.0, tvOS 10.0, *)) {
XCTAssertEqual(self.requests.count, 0);
return;
}
XCTAssertEqual(self.scheduledLocalNotifications.count, 0);
#endif
}
Expand All @@ -120,7 +151,6 @@ - (void)testMessageWithPastEndTime {
*/
- (void)testMessageWithPastStartAndFutureEndTime {
#if TARGET_OS_IOS
NSString *messageIdentifier = @"fcm-cm-test1";
NSDate *startDate = [NSDate dateWithTimeIntervalSinceNow:-1000]; // past
NSDate *endDate = [NSDate dateWithTimeIntervalSinceNow:1000]; // future
NSString *startTimeString = [self.dateFormatter stringFromDate:startDate];
Expand All @@ -129,13 +159,23 @@ - (void)testMessageWithPastStartAndFutureEndTime {
NSDictionary *message = @{
kFIRMessagingContextManagerLocalTimeStart : startTimeString,
kFIRMessagingContextManagerLocalTimeEnd : endTimeString,
kFIRMessagingContextManagerBodyKey : @"Hello world!",
@"id" : messageIdentifier,
@"hello" : @"world"
kFIRMessagingContextManagerBodyKey : kBody,
kMessageIdentifierKey : kMessageIdentifierValue,
kUserInfoKey1 : kUserInfoValue1,
kUserInfoKey2 : kUserInfoValue2
};

XCTAssertTrue([FIRMessagingContextManagerService handleContextManagerMessage:message]);

if (@available(macOS 10.14, iOS 10.0, watchOS 3.0, tvOS 10.0, *)) {
XCTAssertEqual(self.requests.count, 1);
UNNotificationRequest *request = self.requests.firstObject;
XCTAssertEqualObjects(request.identifier, kMessageIdentifierValue);
XCTAssertEqualObjects(request.content.body, kBody);
XCTAssertEqualObjects(request.content.userInfo[kUserInfoKey1], kUserInfoValue1);
XCTAssertEqualObjects(request.content.userInfo[kUserInfoKey2], kUserInfoValue2);
return;
}
XCTAssertEqual(self.scheduledLocalNotifications.count, 1);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
Expand All @@ -145,6 +185,8 @@ - (void)testMessageWithPastStartAndFutureEndTime {
XCTAssertEqual([notification.fireDate compare:startDate], NSOrderedDescending);
// schedule notification after end date
XCTAssertEqual([notification.fireDate compare:endDate], NSOrderedAscending);
XCTAssertEqualObjects(notification.userInfo[kUserInfoKey1], kUserInfoValue1);
XCTAssertEqualObjects(notification.userInfo[kUserInfoKey2], kUserInfoValue2);
#endif
}

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

NSString *customDataKey = @"hello";
NSString *customData = @"world";
NSDictionary *message = @{
kFIRMessagingContextManagerLocalTimeStart : startTimeString,
kFIRMessagingContextManagerBodyKey : @"Hello world!",
messageIdentifierKey : messageIdentifier,
customDataKey : customData,
kFIRMessagingContextManagerBodyKey : kBody,
kMessageIdentifierKey : kMessageIdentifierValue,
kUserInfoKey1 : kUserInfoValue1,
kUserInfoKey2 : kUserInfoValue2
};

XCTAssertTrue([FIRMessagingContextManagerService handleContextManagerMessage:message]);

if (@available(macOS 10.14, iOS 10.0, watchOS 3.0, tvOS 10.0, *)) {
XCTAssertEqual(self.requests.count, 1);
UNNotificationRequest *request = self.requests.firstObject;
XCTAssertEqualObjects(request.identifier, kMessageIdentifierValue);
XCTAssertEqualObjects(request.content.body, kBody);
XCTAssertEqualObjects(request.content.userInfo[kUserInfoKey1], kUserInfoValue1);
XCTAssertEqualObjects(request.content.userInfo[kUserInfoKey2], kUserInfoValue2);
return;
}
XCTAssertEqual(self.scheduledLocalNotifications.count, 1);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
UILocalNotification *notification = [self.scheduledLocalNotifications firstObject];
#pragma clang diagnostic pop
XCTAssertEqualObjects(notification.userInfo[messageIdentifierKey], messageIdentifier);
XCTAssertEqualObjects(notification.userInfo[customDataKey], customData);
XCTAssertEqualObjects(notification.userInfo[kUserInfoKey1], kUserInfoValue1);
XCTAssertEqualObjects(notification.userInfo[kUserInfoKey2], kUserInfoValue2);
#endif
}

#pragma mark - Private Helpers

- (void)mockSchedulingLocalNotifications {
if (@available(macOS 10.14, iOS 10.0, watchOS 3.0, tvOS 10.0, *)) {
id mockNotificationCenter =
OCMPartialMock([UNUserNotificationCenter currentNotificationCenter]);
__block UNNotificationRequest *request;
[[[mockNotificationCenter stub] andDo:^(NSInvocation *invocation) {
[self.requests addObject:request];
}] addNotificationRequest:[OCMArg checkWithBlock:^BOOL(id obj) {
if ([obj isKindOfClass:[UNNotificationRequest class]]) {
request = obj;
[self.requests addObject:request];
return YES;
}
return NO;
}]
withCompletionHandler:^(NSError *_Nullable error){
}];
return;
}
#if TARGET_OS_IOS
id mockApplication = OCMPartialMock([UIApplication sharedApplication]);
#pragma clang diagnostic push
Expand Down