Push from command line

This commit is contained in:
Deniz Duezgoeren
2019-08-12 11:20:21 +02:00
parent 3a919dcb23
commit f1345eac14
512 changed files with 103288 additions and 1930 deletions
@@ -0,0 +1,199 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
typedef NS_ENUM(NSInteger, FIRMessagingMessageCode) {
// FIRMessaging+FIRApp.m
kFIRMessagingMessageCodeFIRApp000 = 1000, // I-FCM001000
kFIRMessagingMessageCodeFIRApp001 = 1001, // I-FCM001001
// FIRMessaging.m
kFIRMessagingMessageCodeMessagingPrintLibraryVersion = 2000, // I-FCM002000
kFIRMessagingMessageCodeMessaging001 = 2001, // I-FCM002001
kFIRMessagingMessageCodeMessaging002 = 2002, // I-FCM002002 - no longer used
kFIRMessagingMessageCodeMessaging003 = 2003, // I-FCM002003
kFIRMessagingMessageCodeMessaging004 = 2004, // I-FCM002004
kFIRMessagingMessageCodeMessaging005 = 2005, // I-FCM002005
kFIRMessagingMessageCodeMessaging006 = 2006, // I-FCM002006 - no longer used
kFIRMessagingMessageCodeMessaging007 = 2007, // I-FCM002007 - no longer used
kFIRMessagingMessageCodeMessaging008 = 2008, // I-FCM002008 - no longer used
kFIRMessagingMessageCodeMessaging009 = 2009, // I-FCM002009
kFIRMessagingMessageCodeMessaging010 = 2010, // I-FCM002010
kFIRMessagingMessageCodeMessaging011 = 2011, // I-FCM002011
kFIRMessagingMessageCodeMessaging012 = 2012, // I-FCM002012
kFIRMessagingMessageCodeMessaging013 = 2013, // I-FCM002013
kFIRMessagingMessageCodeMessaging014 = 2014, // I-FCM002014
kFIRMessagingMessageCodeMessaging015 = 2015, // I-FCM002015
kFIRMessagingMessageCodeMessaging016 = 2016, // I-FCM002016 - no longer used
kFIRMessagingMessageCodeMessaging017 = 2017, // I-FCM002017
kFIRMessagingMessageCodeMessaging018 = 2018, // I-FCM002018
kFIRMessagingMessageCodeRemoteMessageDelegateMethodNotImplemented = 2019, // I-FCM002019
kFIRMessagingMessageCodeSenderIDNotSuppliedForTokenFetch = 2020, // I-FCM002020
kFIRMessagingMessageCodeSenderIDNotSuppliedForTokenDelete = 2021, // I-FCM002021
kFIRMessagingMessageCodeAPNSTokenNotAvailableDuringTokenFetch = 2022, // I-FCM002022
kFIRMessagingMessageCodeTokenDelegateMethodsNotImplemented = 2023, // I-FCM002023
kFIRMessagingMessageCodeTopicFormatIsDeprecated = 2024,
kFIRMessagingMessageCodeDirectChannelConnectionFailed = 2025,
// FIRMessagingClient.m
kFIRMessagingMessageCodeClient000 = 4000, // I-FCM004000
kFIRMessagingMessageCodeClient001 = 4001, // I-FCM004001
kFIRMessagingMessageCodeClient002 = 4002, // I-FCM004002
kFIRMessagingMessageCodeClient003 = 4003, // I-FCM004003
kFIRMessagingMessageCodeClient004 = 4004, // I-FCM004004
kFIRMessagingMessageCodeClient005 = 4005, // I-FCM004005
kFIRMessagingMessageCodeClient006 = 4006, // I-FCM004006
kFIRMessagingMessageCodeClient007 = 4007, // I-FCM004007
kFIRMessagingMessageCodeClient008 = 4008, // I-FCM004008
kFIRMessagingMessageCodeClient009 = 4009, // I-FCM004009
kFIRMessagingMessageCodeClient010 = 4010, // I-FCM004010
kFIRMessagingMessageCodeClient011 = 4011, // I-FCM004011
// FIRMessagingConnection.m
kFIRMessagingMessageCodeConnection000 = 5000, // I-FCM005000
kFIRMessagingMessageCodeConnection001 = 5001, // I-FCM005001
kFIRMessagingMessageCodeConnection002 = 5002, // I-FCM005002
kFIRMessagingMessageCodeConnection003 = 5003, // I-FCM005003
kFIRMessagingMessageCodeConnection004 = 5004, // I-FCM005004
kFIRMessagingMessageCodeConnection005 = 5005, // I-FCM005005
kFIRMessagingMessageCodeConnection006 = 5006, // I-FCM005006
kFIRMessagingMessageCodeConnection007 = 5007, // I-FCM005007
kFIRMessagingMessageCodeConnection008 = 5008, // I-FCM005008
kFIRMessagingMessageCodeConnection009 = 5009, // I-FCM005009
kFIRMessagingMessageCodeConnection010 = 5010, // I-FCM005010
kFIRMessagingMessageCodeConnection011 = 5011, // I-FCM005011
kFIRMessagingMessageCodeConnection012 = 5012, // I-FCM005012
kFIRMessagingMessageCodeConnection013 = 5013, // I-FCM005013
kFIRMessagingMessageCodeConnection014 = 5014, // I-FCM005014
kFIRMessagingMessageCodeConnection015 = 5015, // I-FCM005015
kFIRMessagingMessageCodeConnection016 = 5016, // I-FCM005016
kFIRMessagingMessageCodeConnection017 = 5017, // I-FCM005017
kFIRMessagingMessageCodeConnection018 = 5018, // I-FCM005018
kFIRMessagingMessageCodeConnection019 = 5019, // I-FCM005019
kFIRMessagingMessageCodeConnection020 = 5020, // I-FCM005020
kFIRMessagingMessageCodeConnection021 = 5021, // I-FCM005021
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
// FIRMessagingDataMessageManager.m
kFIRMessagingMessageCodeDataMessageManager000 = 7000, // I-FCM007000
kFIRMessagingMessageCodeDataMessageManager001 = 7001, // I-FCM007001
kFIRMessagingMessageCodeDataMessageManager002 = 7002, // I-FCM007002
kFIRMessagingMessageCodeDataMessageManager003 = 7003, // I-FCM007003
kFIRMessagingMessageCodeDataMessageManager004 = 7004, // I-FCM007004
kFIRMessagingMessageCodeDataMessageManager005 = 7005, // I-FCM007005
kFIRMessagingMessageCodeDataMessageManager006 = 7006, // I-FCM007006
kFIRMessagingMessageCodeDataMessageManager007 = 7007, // I-FCM007007
kFIRMessagingMessageCodeDataMessageManager008 = 7008, // I-FCM007008
kFIRMessagingMessageCodeDataMessageManager009 = 7009, // I-FCM007009
kFIRMessagingMessageCodeDataMessageManager010 = 7010, // I-FCM007010
kFIRMessagingMessageCodeDataMessageManager011 = 7011, // I-FCM007011
kFIRMessagingMessageCodeDataMessageManager012 = 7012, // I-FCM007012
// FIRMessagingPendingTopicsList.m
kFIRMessagingMessageCodePendingTopicsList000 = 8000, // I-FCM008000
// FIRMessagingPubSub.m
kFIRMessagingMessageCodePubSub000 = 9000, // I-FCM009000
kFIRMessagingMessageCodePubSub001 = 9001, // I-FCM009001
kFIRMessagingMessageCodePubSub002 = 9002, // I-FCM009002
kFIRMessagingMessageCodePubSub003 = 9003, // I-FCM009003
// FIRMessagingReceiver.m
kFIRMessagingMessageCodeReceiver000 = 10000, // I-FCM010000
kFIRMessagingMessageCodeReceiver001 = 10001, // I-FCM010001
kFIRMessagingMessageCodeReceiver002 = 10002, // I-FCM010002
kFIRMessagingMessageCodeReceiver003 = 10003, // I-FCM010003
kFIRMessagingMessageCodeReceiver004 = 10004, // I-FCM010004 - no longer used
kFIRMessagingMessageCodeReceiver005 = 10005, // I-FCM010005
// FIRMessagingRegistrar.m
kFIRMessagingMessageCodeRegistrar000 = 11000, // I-FCM011000
// FIRMessagingRemoteNotificationsProxy.m
kFIRMessagingMessageCodeRemoteNotificationsProxy000 = 12000, // I-FCM012000
kFIRMessagingMessageCodeRemoteNotificationsProxy001 = 12001, // I-FCM012001
kFIRMessagingMessageCodeRemoteNotificationsProxyAPNSFailed = 12002, // I-FCM012002
kFIRMessagingMessageCodeRemoteNotificationsProxyMethodNotAdded = 12003, // I-FCM012003
// FIRMessagingRmq2PersistentStore.m
kFIRMessagingMessageCodeRmq2PersistentStore000 = 13000, // I-FCM013000
kFIRMessagingMessageCodeRmq2PersistentStore001 = 13001, // I-FCM013001
kFIRMessagingMessageCodeRmq2PersistentStore002 = 13002, // I-FCM013002
kFIRMessagingMessageCodeRmq2PersistentStore003 = 13003, // I-FCM013003
kFIRMessagingMessageCodeRmq2PersistentStore004 = 13004, // I-FCM013004
kFIRMessagingMessageCodeRmq2PersistentStore005 = 13005, // I-FCM013005
kFIRMessagingMessageCodeRmq2PersistentStore006 = 13006, // I-FCM013006
kFIRMessagingMessageCodeRmq2PersistentStoreErrorCreatingDatabase = 13007, // I-FCM013007
kFIRMessagingMessageCodeRmq2PersistentStoreErrorOpeningDatabase = 13008, // I-FCM013008
kFIRMessagingMessageCodeRmq2PersistentStoreInvalidRmqDirectory = 13009, // I-FCM013009
kFIRMessagingMessageCodeRmq2PersistentStoreErrorCreatingTable = 13010, // I-FCM013010
// FIRMessagingRmqManager.m
kFIRMessagingMessageCodeRmqManager000 = 14000, // I-FCM014000
// FIRMessagingSecureSocket.m
kFIRMessagingMessageCodeSecureSocket000 = 15000, // I-FCM015000
kFIRMessagingMessageCodeSecureSocket001 = 15001, // I-FCM015001
kFIRMessagingMessageCodeSecureSocket002 = 15002, // I-FCM015002
kFIRMessagingMessageCodeSecureSocket003 = 15003, // I-FCM015003
kFIRMessagingMessageCodeSecureSocket004 = 15004, // I-FCM015004
kFIRMessagingMessageCodeSecureSocket005 = 15005, // I-FCM015005
kFIRMessagingMessageCodeSecureSocket006 = 15006, // I-FCM015006
kFIRMessagingMessageCodeSecureSocket007 = 15007, // I-FCM015007
kFIRMessagingMessageCodeSecureSocket008 = 15008, // I-FCM015008
kFIRMessagingMessageCodeSecureSocket009 = 15009, // I-FCM015009
kFIRMessagingMessageCodeSecureSocket010 = 15010, // I-FCM015010
kFIRMessagingMessageCodeSecureSocket011 = 15011, // I-FCM015011
kFIRMessagingMessageCodeSecureSocket012 = 15012, // I-FCM015012
kFIRMessagingMessageCodeSecureSocket013 = 15013, // I-FCM015013
kFIRMessagingMessageCodeSecureSocket014 = 15014, // I-FCM015014
kFIRMessagingMessageCodeSecureSocket015 = 15015, // I-FCM015015
kFIRMessagingMessageCodeSecureSocket016 = 15016, // I-FCM015016
// FIRMessagingSyncMessageManager.m
kFIRMessagingMessageCodeSyncMessageManager000 = 16000, // I-FCM016000
kFIRMessagingMessageCodeSyncMessageManager001 = 16001, // I-FCM016001
kFIRMessagingMessageCodeSyncMessageManager002 = 16002, // I-FCM016002
kFIRMessagingMessageCodeSyncMessageManager003 = 16003, // I-FCM016003
kFIRMessagingMessageCodeSyncMessageManager004 = 16004, // I-FCM016004
kFIRMessagingMessageCodeSyncMessageManager005 = 16005, // I-FCM016005
kFIRMessagingMessageCodeSyncMessageManager006 = 16006, // I-FCM016006
kFIRMessagingMessageCodeSyncMessageManager007 = 16007, // I-FCM016007
kFIRMessagingMessageCodeSyncMessageManager008 = 16008, // I-FCM016008
// FIRMessagingTopicOperation.m
kFIRMessagingMessageCodeTopicOption000 = 17000, // I-FCM017000
kFIRMessagingMessageCodeTopicOption001 = 17001, // I-FCM017001
kFIRMessagingMessageCodeTopicOption002 = 17002, // I-FCM017002
kFIRMessagingMessageCodeTopicOptionTopicEncodingFailed = 17003, // I-FCM017003
kFIRMessagingMessageCodeTopicOperationEmptyResponse = 17004, // I-FCM017004
// FIRMessagingUtilities.m
kFIRMessagingMessageCodeUtilities000 = 18000, // I-FCM018000
kFIRMessagingMessageCodeUtilities001 = 18001, // I-FCM018001
kFIRMessagingMessageCodeUtilities002 = 18002, // I-FCM018002
// FIRMessagingAnalytics.m
kFIRMessagingMessageCodeAnalytics000 = 19000, // I-FCM019000
kFIRMessagingMessageCodeAnalytics001 = 19001, // I-FCM019001
kFIRMessagingMessageCodeAnalytics002 = 19002, // I-FCM019002
kFIRMessagingMessageCodeAnalytics003 = 19003, // I-FCM019003
kFIRMessagingMessageCodeAnalytics004 = 19004, // I-FCM019004
kFIRMessagingMessageCodeAnalytics005 = 19005, // I-FCM019005
kFIRMessagingMessageCodeAnalyticsInvalidEvent = 19006, // I-FCM019006
kFIRMessagingMessageCodeAnalytics007 = 19007, // I-FCM019007
kFIRMessagingMessageCodeAnalyticsCouldNotInvokeAnalyticsLog = 19008, // I-FCM019008
// FIRMessagingExtensionHelper.m
kFIRMessagingServiceExtensionImageInvalidURL = 20000,
kFIRMessagingServiceExtensionImageNotDownloaded = 20001,
kFIRMessagingServiceExtensionLocalFileNotCreated = 20002,
kFIRMessagingServiceExtensionImageNotAttached = 20003,
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,44 @@
/*
* Copyright 2018 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
#import <FirebaseAnalyticsInterop/FIRAnalyticsInterop.h>
NS_ASSUME_NONNULL_BEGIN
/**
* Provides integration between FIRMessaging and Analytics.
*
* All Analytics dependencies should be kept in this class, and missing dependencies should be
* handled gracefully.
*
*/
@interface FIRMessagingAnalytics : NSObject
/**
* Determine whether a notification has the properties to be loggable to Analytics.
* If so, send the notification.
* @param notification The notification payload from APNs
* @param analytics The class to be used as the receiver of the logging method
*/
+ (void)logMessage:(NSDictionary *)notification
toAnalytics:(id<FIRAnalyticsInterop> _Nullable)analytics;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,216 @@
/*
* Copyright 2018 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessagingAnalytics.h"
#import "FIRMessagingLogger.h"
#import <FirebaseAnalyticsInterop/FIRInteropEventNames.h>
#import <FirebaseAnalyticsInterop/FIRInteropParameterNames.h>
#import <GoogleUtilities/GULAppEnvironmentUtil.h>
#import <GoogleUtilities/GULAppDelegateSwizzler.h>
static NSString *const kLogTag = @"FIRMessagingAnalytics";
// aps Key
static NSString *const kApsKey = @"aps";
static NSString *const kApsAlertKey = @"alert";
static NSString *const kApsSoundKey = @"sound";
static NSString *const kApsBadgeKey = @"badge";
static NSString *const kApsContentAvailableKey = @"badge";
// Data Key
static NSString *const kDataKey = @"data";
// Messaging From Key
static NSString *const kFIRMessagingFromKey = @"from";
static NSString *const kFIRParameterLabel = @"label";
static NSString *const kReengagementSource = @"Firebase";
static NSString *const kReengagementMedium = @"notification";
// Analytics
static NSString *const kAnalyticsEnabled = @"google.c.a." @"e";
static NSString *const kAnalyticsComposerIdentifier = @"google.c.a." @"c_id";
static NSString *const kAnalyticsComposerLabel = @"google.c.a." @"c_l";
static NSString *const kAnalyticsMessageLabel = @"google.c.a." @"m_l";
static NSString *const kAnalyticsMessageTimestamp = @"google.c.a." @"ts";
static NSString *const kAnalyticsMessageUseDeviceTime = @"google.c.a." @"udt";
static NSString *const kAnalyticsTrackConversions = @"google.c.a." @"tc";
@implementation FIRMessagingAnalytics
+ (BOOL)canLogNotification:(NSDictionary *)notification {
if (!notification.count) {
// Payload is empty
return NO;
}
NSString *isAnalyticsLoggingEnabled = notification[kAnalyticsEnabled];
if (![isAnalyticsLoggingEnabled isKindOfClass:[NSString class]] ||
![isAnalyticsLoggingEnabled isEqualToString:@"1"]) {
// Analytics logging is not enabled
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeAnalytics001,
@"Analytics logging is disabled. Do not log event.");
return NO;
}
return YES;
}
+ (void)logOpenNotification:(NSDictionary *)notification
toAnalytics:(id<FIRAnalyticsInterop> _Nullable)analytics {
[self logUserPropertyForConversionTracking:notification toAnalytics:analytics];
[self logEvent:kFIRIEventNotificationOpen
withNotification:notification
toAnalytics:analytics];
}
+ (void)logForegroundNotification:(NSDictionary *)notification
toAnalytics:(id<FIRAnalyticsInterop> _Nullable)analytics {
[self logEvent:kFIRIEventNotificationForeground
withNotification:notification
toAnalytics:analytics];
}
+ (void)logEvent:(NSString *)event
withNotification:(NSDictionary *)notification
toAnalytics:(id<FIRAnalyticsInterop> _Nullable)analytics {
if (!event.length) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeAnalyticsInvalidEvent,
@"Can't log analytics with empty event.");
return;
}
NSMutableDictionary *params = [self paramsForEvent:event withNotification:notification];
[analytics logEventWithOrigin:@"fcm" name:event parameters:params];
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeAnalytics005,
@"%@: Sending event: %@ params: %@", kLogTag, event, params);
}
+ (NSMutableDictionary *)paramsForEvent:(NSString *)event
withNotification:(NSDictionary *)notification {
NSDictionary *analyticsDataMap = notification;
if (!analyticsDataMap.count) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeAnalytics000,
@"No data found in notification. Will not log any analytics events.");
return nil;
}
if (![self canLogNotification:analyticsDataMap]) {
return nil;
}
NSMutableDictionary *params = [NSMutableDictionary dictionary];
NSString *composerIdentifier = analyticsDataMap[kAnalyticsComposerIdentifier];
if ([composerIdentifier isKindOfClass:[NSString class]] && composerIdentifier.length) {
params[kFIRIParameterMessageIdentifier] = [composerIdentifier copy];
}
NSString *composerLabel = analyticsDataMap[kAnalyticsComposerLabel];
if ([composerLabel isKindOfClass:[NSString class]] && composerLabel.length) {
params[kFIRIParameterMessageName] = [composerLabel copy];
}
NSString *messageLabel = analyticsDataMap[kAnalyticsMessageLabel];
if ([messageLabel isKindOfClass:[NSString class]] && messageLabel.length) {
params[kFIRParameterLabel] = [messageLabel copy];
}
NSString *from = analyticsDataMap[kFIRMessagingFromKey];
if ([from isKindOfClass:[NSString class]] && [from containsString:@"/topics/"]) {
params[kFIRIParameterTopic] = [from copy];
}
id timestamp = analyticsDataMap[kAnalyticsMessageTimestamp];
if ([timestamp respondsToSelector:@selector(longLongValue)]) {
int64_t timestampValue = [timestamp longLongValue];
if (timestampValue != 0) {
params[kFIRIParameterMessageTime] = @(timestampValue);
}
}
if (analyticsDataMap[kAnalyticsMessageUseDeviceTime]) {
params[kFIRIParameterMessageDeviceTime] = analyticsDataMap[kAnalyticsMessageUseDeviceTime];
}
return params;
}
+ (void)logUserPropertyForConversionTracking:(NSDictionary *)notification
toAnalytics:(id<FIRAnalyticsInterop> _Nullable)analytics {
NSInteger shouldTrackConversions = [notification[kAnalyticsTrackConversions] integerValue];
if (shouldTrackConversions != 1) {
return;
}
NSString *composerIdentifier = notification[kAnalyticsComposerIdentifier];
if ([composerIdentifier isKindOfClass:[NSString class]] && composerIdentifier.length) {
// Set user property for event.
[analytics setUserPropertyWithOrigin:@"fcm"
name:kFIRIUserPropertyLastNotification
value:composerIdentifier];
// Set the re-engagement attribution properties.
NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity:3];
params[kFIRIParameterSource] = kReengagementSource;
params[kFIRIParameterMedium] = kReengagementMedium;
params[kFIRIParameterCampaign] = composerIdentifier;
[analytics logEventWithOrigin:@"fcm" name:kFIRIEventFirebaseCampaign parameters:params];
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeAnalytics003,
@"%@: Sending event: %@ params: %@", kLogTag,
kFIRIEventFirebaseCampaign, params);
} else {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeAnalytics004,
@"%@: Failed to set user property: %@ value: %@", kLogTag,
kFIRIUserPropertyLastNotification, composerIdentifier);
}
}
+ (void)logMessage:(NSDictionary *)notification
toAnalytics:(id<FIRAnalyticsInterop> _Nullable)analytics {
// iOS onlly because Analytics doesn't support tvOS.
#if TARGET_OS_IOS
if (![self canLogNotification:notification]) {
return;
}
UIApplication *application = [GULAppDelegateSwizzler sharedApplication];
if (!application) {
return;
}
UIApplicationState applicationState = application.applicationState;
switch (applicationState) {
case UIApplicationStateInactive:
// App was either in background(suspended) or inactive and user tapped on a display
// notification.
[self logOpenNotification:notification toAnalytics:analytics];
break;
case UIApplicationStateActive:
// App was in foreground when it received the notification.
[self logForegroundNotification:notification toAnalytics:analytics];
break;
default:
// Only a silent notification (i.e. 'content-available' is true) can be received while the app
// is in the background. These messages aren't loggable anyway.
break;
}
#endif
}
@end
@@ -0,0 +1,156 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessaging.h"
@class GULReachabilityChecker;
@class GPBMessage;
@class FIRMessagingConnection;
@class FIRMessagingDataMessageManager;
@class FIRMessagingRmqManager;
/**
* Callback to handle MCS connection requests.
*
* @param error The error object if any while trying to connect with MCS else nil.
*/
typedef void(^FIRMessagingConnectCompletionHandler)(NSError *error);
@protocol FIRMessagingClientDelegate <NSObject>
@end
/**
* The client handles the subscribe/unsubscribe for an unregistered senderID
* and device. It also manages the FIRMessaging data connection, the exponential backoff
* algorithm in case of registration failures, sign in failures and unregister
* failures. It also handles the reconnect logic if the FIRMessaging connection is
* broken off by some error during an active session.
*/
@interface FIRMessagingClient : NSObject
@property(nonatomic, readonly, strong) FIRMessagingConnection *connection;
@property(nonatomic, readwrite, weak) FIRMessagingDataMessageManager *dataMessageManager;
// Designated initializer
- (instancetype)initWithDelegate:(id<FIRMessagingClientDelegate>)delegate
reachability:(GULReachabilityChecker *)reachability
rmq2Manager:(FIRMessagingRmqManager *)rmq2Manager;
- (void)teardown;
- (void)cancelAllRequests;
#pragma mark - FIRMessaging subscribe
/**
* Update the subscription associated with the given token and topic.
*
* For a to-be-created subscription we check if the client is already
* subscribed to the topic or not. If subscribed we should have the
* subscriptionID in the cache and we return from there itself, else we call
* the FIRMessaging backend to create a new subscription for the topic for this client.
*
* For delete subscription requests we delete the stored subscription in the
* client and then invoke the FIRMessaging backend to delete the existing subscription
* completely.
*
* @param token The token associated with the device.
* @param topic The topic for which the subscription should be updated.
* @param options The options to be passed in to the subscription request.
* @param shouldDelete If YES this would delete the subscription from the cache
* and also let the FIRMessaging backend know that we need to delete
* the subscriptionID associated with this topic.
* If NO we try to create a new subscription for the given
* token and topic.
* @param handler The handler to invoke once the subscription request
* finishes.
*/
- (void)updateSubscriptionWithToken:(NSString *)token
topic:(NSString *)topic
options:(NSDictionary *)options
shouldDelete:(BOOL)shouldDelete
handler:(FIRMessagingTopicOperationCompletion)handler;
#pragma mark - MCS Connection
/**
* Create a MCS connection.
*
* @param handler The handler to be invokend once the connection is setup. If
* setting up the connection fails we invoke the handler with
* an appropriate error object.
*/
- (void)connectWithHandler:(FIRMessagingConnectCompletionHandler)handler;
/**
* Disconnect the current MCS connection. If there is no valid connection this
* should be a NO-OP.
*/
- (void)disconnect;
#pragma mark - MCS Connection State
/**
* If we are connected to MCS or not. This doesn't take into account the fact if
* the client has been signed in(verified) by MCS.
*
* @return YES if we are signed in or connecting and trying to sign-in else NO.
*/
@property(nonatomic, readonly) BOOL isConnected;
/**
* If we have an active MCS connection
*
* @return YES if we have an active MCS connection else NO.
*/
@property(nonatomic, readonly) BOOL isConnectionActive;
/**
* If we should be connected to MCS
*
* @return YES if we have attempted a connection and not requested to disconect.
*/
@property(nonatomic, readonly) BOOL shouldStayConnected;
/**
* Schedule a retry to connect to MCS. If `immediately` is `YES` try to
* schedule a retry now else retry with some delay.
*
* @param immediately Should retry right now.
*/
- (void)retryConnectionImmediately:(BOOL)immediately;
#pragma mark - Messages
/**
* Send a message over the MCS connection.
*
* @param message Message to be sent.
*/
- (void)sendMessage:(GPBMessage *)message;
/**
* Send message if we have an active MCS connection. If not cache the message
* for this session and in case we are able to re-establish the connection try
* again else drop it. This should only be used for TTL=0 messages for now.
*
* @param message Message to be sent.
*/
- (void)sendOnConnectOrDrop:(GPBMessage *)message;
@end
@@ -0,0 +1,513 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessagingClient.h"
#import <FirebaseInstanceID/FIRInstanceID_Private.h>
#import <GoogleUtilities/GULReachabilityChecker.h>
#import "FIRMessaging.h"
#import "FIRMessagingConnection.h"
#import "FIRMessagingConstants.h"
#import "FIRMessagingDataMessageManager.h"
#import "FIRMessagingDefines.h"
#import "FIRMessagingLogger.h"
#import "FIRMessagingRmqManager.h"
#import "FIRMessagingTopicsCommon.h"
#import "FIRMessagingUtilities.h"
#import "NSError+FIRMessaging.h"
#import "FIRMessagingPubSubRegistrar.h"
static const NSTimeInterval kConnectTimeoutInterval = 40.0;
static const NSTimeInterval kReconnectDelayInSeconds = 2 * 60; // 2 minutes
static const NSUInteger kMaxRetryExponent = 10; // 2^10 = 1024 seconds ~= 17 minutes
static NSString *const kFIRMessagingMCSServerHost = @"mtalk.google.com";
static NSUInteger const kFIRMessagingMCSServerPort = 5228;
// register device with checkin
typedef void(^FIRMessagingRegisterDeviceHandler)(NSError *error);
static NSString *FIRMessagingServerHost() {
static NSString *serverHost = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSDictionary *environment = [[NSProcessInfo processInfo] environment];
NSString *customServerHostAndPort = environment[@"FCM_MCS_HOST"];
NSString *host = [customServerHostAndPort componentsSeparatedByString:@":"].firstObject;
if (host) {
serverHost = host;
} else {
serverHost = kFIRMessagingMCSServerHost;
}
});
return serverHost;
}
static NSUInteger FIRMessagingServerPort() {
static NSUInteger serverPort = kFIRMessagingMCSServerPort;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSDictionary *environment = [[NSProcessInfo processInfo] environment];
NSString *customServerHostAndPort = environment[@"FCM_MCS_HOST"];
NSArray<NSString *> *components = [customServerHostAndPort componentsSeparatedByString:@":"];
NSUInteger port = (NSUInteger)[components.lastObject integerValue];
if (port != 0) {
serverPort = port;
}
});
return serverPort;
}
@interface FIRMessagingClient () <FIRMessagingConnectionDelegate>
@property(nonatomic, readwrite, weak) id<FIRMessagingClientDelegate> clientDelegate;
@property(nonatomic, readwrite, strong) FIRMessagingConnection *connection;
@property(nonatomic, readonly, strong) FIRMessagingPubSubRegistrar *registrar;
@property(nonatomic, readwrite, strong) NSString *senderId;
// FIRMessagingService owns these instances
@property(nonatomic, readwrite, weak) FIRMessagingRmqManager *rmq2Manager;
@property(nonatomic, readwrite, weak) GULReachabilityChecker *reachability;
@property(nonatomic, readwrite, assign) int64_t lastConnectedTimestamp;
@property(nonatomic, readwrite, assign) int64_t lastDisconnectedTimestamp;
@property(nonatomic, readwrite, assign) NSUInteger connectRetryCount;
// Should we stay connected to MCS or not. Should be YES throughout the lifetime
// of a MCS connection. If set to NO it signifies that an existing MCS connection
// should be disconnected.
@property(nonatomic, readwrite, assign) BOOL stayConnected;
@property(nonatomic, readwrite, assign) NSTimeInterval connectionTimeoutInterval;
// Used if the MCS connection suddenly breaksdown in the middle and we want to reconnect
// with some permissible delay we schedule a reconnect and set it to YES and when it's
// scheduled this will be set back to NO.
@property(nonatomic, readwrite, assign) BOOL didScheduleReconnect;
// handlers
@property(nonatomic, readwrite, copy) FIRMessagingConnectCompletionHandler connectHandler;
@end
@implementation FIRMessagingClient
- (instancetype)init {
FIRMessagingInvalidateInitializer();
}
- (instancetype)initWithDelegate:(id<FIRMessagingClientDelegate>)delegate
reachability:(GULReachabilityChecker *)reachability
rmq2Manager:(FIRMessagingRmqManager *)rmq2Manager {
self = [super init];
if (self) {
_reachability = reachability;
_clientDelegate = delegate;
_rmq2Manager = rmq2Manager;
_registrar = [[FIRMessagingPubSubRegistrar alloc] init];
_connectionTimeoutInterval = kConnectTimeoutInterval;
// Listen for checkin fetch notifications, as connecting to MCS may have failed due to
// missing checkin info (while it was being fetched).
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(checkinFetched:)
name:kFIRMessagingCheckinFetchedNotification
object:nil];
}
return self;
}
- (void)teardown {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient000, @"");
self.stayConnected = NO;
// Clear all the handlers
self.connectHandler = nil;
[self.connection teardown];
// Stop all subscription requests
[self.registrar stopAllSubscriptionRequests];
_FIRMessagingDevAssert(self.connection.state == kFIRMessagingConnectionNotConnected, @"Did not disconnect");
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)cancelAllRequests {
// Stop any checkin requests or any subscription requests
[self.registrar stopAllSubscriptionRequests];
// Stop any future connection requests to MCS
if (self.stayConnected && self.isConnected && !self.isConnectionActive) {
self.stayConnected = NO;
[NSObject cancelPreviousPerformRequestsWithTarget:self];
}
}
#pragma mark - FIRMessaging subscribe
- (void)updateSubscriptionWithToken:(NSString *)token
topic:(NSString *)topic
options:(NSDictionary *)options
shouldDelete:(BOOL)shouldDelete
handler:(FIRMessagingTopicOperationCompletion)handler {
_FIRMessagingDevAssert(handler != nil, @"Invalid handler to FIRMessaging subscribe");
FIRMessagingTopicOperationCompletion completion = ^void(NSError *error) {
if (error) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeClient001, @"Failed to subscribe to topic %@",
error);
} else {
if (shouldDelete) {
FIRMessagingLoggerInfo(kFIRMessagingMessageCodeClient002,
@"Successfully unsubscribed from topic %@", topic);
} else {
FIRMessagingLoggerInfo(kFIRMessagingMessageCodeClient003,
@"Successfully subscribed to topic %@", topic);
}
}
handler(error);
};
if ([[FIRInstanceID instanceID] tryToLoadValidCheckinInfo]) {
[self.registrar updateSubscriptionToTopic:topic
withToken:token
options:options
shouldDelete:shouldDelete
handler:completion];
} else {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRegistrar000,
@"Device check in error, no auth credentials found");
NSError *error = [NSError errorWithFCMErrorCode:kFIRMessagingErrorCodeMissingDeviceID];
handler(error);
}
}
#pragma mark - MCS Connection
- (BOOL)isConnected {
return self.stayConnected && self.connection.state != kFIRMessagingConnectionNotConnected;
}
- (BOOL)isConnectionActive {
return self.stayConnected && self.connection.state == kFIRMessagingConnectionSignedIn;
}
- (BOOL)shouldStayConnected {
return self.stayConnected;
}
- (void)retryConnectionImmediately:(BOOL)immediately {
// Do not connect to an invalid host or an invalid port
if (!self.stayConnected || !self.connection.host || self.connection.port == 0) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient004,
@"FIRMessaging connection will not reconnect to MCS. "
@"Stay connected: %d",
self.stayConnected);
return;
}
if (self.isConnectionActive) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient005,
@"FIRMessaging Connection skip retry, active");
// already connected and logged in.
// Heartbeat alarm is set and will force close the connection
return;
}
if (self.isConnected) {
// already connected and logged in.
// Heartbeat alarm is set and will force close the connection
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient006,
@"FIRMessaging Connection skip retry, connected");
return;
}
if (immediately) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient007,
@"Try to connect to MCS immediately");
[self tryToConnect];
} else {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient008, @"Try to connect to MCS lazily");
// Avoid all the other logic that we have in other clients, since this would always happen
// when the app is in the foreground and since the FIRMessaging connection isn't shared with any other
// app we can be more aggressive in reconnections
if (!self.didScheduleReconnect) {
FIRMessaging_WEAKIFY(self);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
(int64_t)(kReconnectDelayInSeconds * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
FIRMessaging_STRONGIFY(self);
self.didScheduleReconnect = NO;
[self tryToConnect];
});
self.didScheduleReconnect = YES;
}
}
}
- (void)connectWithHandler:(FIRMessagingConnectCompletionHandler)handler {
if (self.isConnected) {
NSError *error = [NSError fcm_errorWithCode:kFIRMessagingErrorCodeAlreadyConnected
userInfo:@{
NSLocalizedFailureReasonErrorKey: @"FIRMessaging is already connected",
}];
handler(error);
return;
}
self.lastDisconnectedTimestamp = FIRMessagingCurrentTimestampInMilliseconds();
self.connectHandler = handler;
[self connect];
}
- (void)connect {
// reset retry counts
self.connectRetryCount = 0;
if (self.isConnected) {
return;
}
self.stayConnected = YES;
if (![[FIRInstanceID instanceID] tryToLoadValidCheckinInfo]) {
// Checkin info is not available. This may be due to the checkin still being fetched.
if (self.connectHandler) {
NSError *error = [NSError errorWithFCMErrorCode:kFIRMessagingErrorCodeMissingDeviceID];
self.connectHandler(error);
}
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient009,
@"Failed to connect to MCS. No deviceID and secret found.");
// Return for now. If checkin is, in fact, retrieved, the
// |kFIRMessagingCheckinFetchedNotification| will be fired.
return;
}
[self setupConnectionAndConnect];
}
- (void)disconnect {
// user called disconnect
// We don't want to connect later even if no network is available.
[self disconnectWithTryToConnectLater:NO];
}
/**
* Disconnect the current client connection. Also explicitly stop and connction retries.
*
* @param tryToConnectLater If YES will try to connect later when sending upstream messages
* else if NO do not connect again until user explicitly calls
* connect.
*/
- (void)disconnectWithTryToConnectLater:(BOOL)tryToConnectLater {
self.stayConnected = tryToConnectLater;
[self.connection signOut];
_FIRMessagingDevAssert(self.connection.state == kFIRMessagingConnectionNotConnected,
@"FIRMessaging connection did not disconnect");
// since we can disconnect while still trying to establish the connection it's required to
// cancel all performSelectors else the object might be retained
[NSObject cancelPreviousPerformRequestsWithTarget:self
selector:@selector(tryToConnect)
object:nil];
[NSObject cancelPreviousPerformRequestsWithTarget:self
selector:@selector(didConnectTimeout)
object:nil];
self.connectHandler = nil;
}
#pragma mark - Checkin Notification
- (void)checkinFetched:(NSNotification *)notification {
// A failed checkin may have been the reason for the connection failure. Attempt a connection
// if the checkin fetched notification is fired.
if (self.stayConnected && !self.isConnected) {
[self connect];
}
}
#pragma mark - Messages
- (void)sendMessage:(GPBMessage *)message {
[self.connection sendProto:message];
}
- (void)sendOnConnectOrDrop:(GPBMessage *)message {
[self.connection sendOnConnectOrDrop:message];
}
#pragma mark - FIRMessagingConnectionDelegate
- (void)connection:(FIRMessagingConnection *)fcmConnection
didCloseForReason:(FIRMessagingConnectionCloseReason)reason {
self.lastDisconnectedTimestamp = FIRMessagingCurrentTimestampInMilliseconds();
if (reason == kFIRMessagingConnectionCloseReasonSocketDisconnected) {
// Cancel the not-yet-triggered timeout task before rescheduling, in case the previous sign in
// failed, due to a connection error caused by bad network.
[NSObject cancelPreviousPerformRequestsWithTarget:self
selector:@selector(didConnectTimeout)
object:nil];
}
if (self.stayConnected) {
[self scheduleConnectRetry];
}
}
- (void)didLoginWithConnection:(FIRMessagingConnection *)fcmConnection {
// Cancel the not-yet-triggered timeout task.
[NSObject cancelPreviousPerformRequestsWithTarget:self
selector:@selector(didConnectTimeout)
object:nil];
self.connectRetryCount = 0;
self.lastConnectedTimestamp = FIRMessagingCurrentTimestampInMilliseconds();
[self.dataMessageManager setDeviceAuthID:[FIRInstanceID instanceID].deviceAuthID
secretToken:[FIRInstanceID instanceID].secretToken];
if (self.connectHandler) {
self.connectHandler(nil);
// notified the third party app with the registrationId.
// we don't want them to know about the connection status and how it changes
// so remove this handler
self.connectHandler = nil;
}
}
- (void)connectionDidRecieveMessage:(GtalkDataMessageStanza *)message {
NSDictionary *parsedMessage = [self.dataMessageManager processPacket:message];
if ([parsedMessage count]) {
[self.dataMessageManager didReceiveParsedMessage:parsedMessage];
}
}
- (int)connectionDidReceiveAckForRmqIds:(NSArray *)rmqIds {
NSSet *rmqIDSet = [NSSet setWithArray:rmqIds];
NSMutableArray *messagesSent = [NSMutableArray arrayWithCapacity:rmqIds.count];
[self.rmq2Manager scanWithRmqMessageHandler:nil
dataMessageHandler:^(int64_t rmqId, GtalkDataMessageStanza *stanza) {
NSString *rmqIdString = [NSString stringWithFormat:@"%lld", rmqId];
if ([rmqIDSet containsObject:rmqIdString]) {
[messagesSent addObject:stanza];
}
}];
for (GtalkDataMessageStanza *message in messagesSent) {
[self.dataMessageManager didSendDataMessageStanza:message];
}
return [self.rmq2Manager removeRmqMessagesWithRmqIds:rmqIds];
}
#pragma mark - Private
- (void)setupConnectionAndConnect {
[self setupConnection];
[self tryToConnect];
}
- (void)setupConnection {
NSString *host = FIRMessagingServerHost();
NSUInteger port = FIRMessagingServerPort();
_FIRMessagingDevAssert([host length] > 0 && port != 0, @"Invalid port or host");
if (self.connection != nil) {
// if there is an old connection, explicitly sign it off.
[self.connection signOut];
self.connection.delegate = nil;
}
self.connection = [[FIRMessagingConnection alloc] initWithAuthID:[FIRInstanceID instanceID].deviceAuthID
token:[FIRInstanceID instanceID].secretToken
host:host
port:port
runLoop:[NSRunLoop mainRunLoop]
rmq2Manager:self.rmq2Manager
fcmManager:self.dataMessageManager];
self.connection.delegate = self;
}
- (void)tryToConnect {
if (!self.stayConnected) {
return;
}
// Cancel any other pending signin requests.
[NSObject cancelPreviousPerformRequestsWithTarget:self
selector:@selector(tryToConnect)
object:nil];
// Do not re-sign in if there is already a connection in progress.
if (self.connection.state != kFIRMessagingConnectionNotConnected) {
return;
}
_FIRMessagingDevAssert([FIRInstanceID instanceID].deviceAuthID.length > 0 &&
[FIRInstanceID instanceID].secretToken.length > 0 &&
self.connection != nil,
@"Invalid state cannot connect");
self.connectRetryCount = MIN(kMaxRetryExponent, self.connectRetryCount + 1);
[self performSelector:@selector(didConnectTimeout)
withObject:nil
afterDelay:self.connectionTimeoutInterval];
[self.connection signIn];
}
- (void)didConnectTimeout {
_FIRMessagingDevAssert(self.connection.state != kFIRMessagingConnectionSignedIn,
@"Invalid state for MCS connection");
if (self.stayConnected) {
[self.connection signOut];
[self scheduleConnectRetry];
}
}
#pragma mark - Schedulers
- (void)scheduleConnectRetry {
GULReachabilityStatus status = self.reachability.reachabilityStatus;
BOOL isReachable = (status == kGULReachabilityViaWifi || status == kGULReachabilityViaCellular);
if (!isReachable) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient010,
@"Internet not reachable when signing into MCS during a retry");
FIRMessagingConnectCompletionHandler handler = [self.connectHandler copy];
// disconnect before issuing a callback
[self disconnectWithTryToConnectLater:YES];
NSError *error =
[NSError errorWithDomain:@"No internet available, cannot connect to FIRMessaging"
code:kFIRMessagingErrorCodeNetwork
userInfo:nil];
if (handler) {
handler(error);
self.connectHandler = nil;
}
return;
}
NSUInteger retryInterval = [self nextRetryInterval];
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient011,
@"Failed to sign in to MCS, retry in %lu seconds",
_FIRMessaging_UL(retryInterval));
[self performSelector:@selector(tryToConnect) withObject:nil afterDelay:retryInterval];
}
- (NSUInteger)nextRetryInterval {
return 1u << self.connectRetryCount;
}
@end
@@ -0,0 +1,28 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
@interface FIRMessagingCodedInputStream : NSObject
@property(nonatomic, readonly, assign) size_t offset;
- (instancetype)initWithData:(NSData *)data;
- (BOOL)readTag:(int8_t *)tag;
- (BOOL)readLength:(int32_t *)length;
- (NSData *)readDataWithLength:(uint32_t)length;
@end
@@ -0,0 +1,142 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessagingCodedInputStream.h"
#import "FIRMessagingDefines.h"
typedef struct {
const void *bytes;
size_t bufferSize;
size_t bufferPos;
} BufferState;
static BOOL CheckSize(BufferState *state, size_t size) {
size_t newSize = state->bufferPos + size;
if (newSize > state->bufferSize) {
return NO;
}
return YES;
}
static BOOL ReadRawByte(BufferState *state, int8_t *output) {
_FIRMessagingDevAssert(output != NULL && state != NULL, @"Invalid parameters");
if (CheckSize(state, sizeof(int8_t))) {
*output = ((int8_t *)state->bytes)[state->bufferPos++];
return YES;
}
return NO;
}
static BOOL ReadRawVarInt32(BufferState *state, int32_t *output) {
_FIRMessagingDevAssert(output != NULL && state != NULL, @"Invalid parameters");
int8_t tmp = 0;
if (!ReadRawByte(state, &tmp)) {
return NO;
}
if (tmp >= 0) {
*output = tmp;
return YES;
}
int32_t result = tmp & 0x7f;
if (!ReadRawByte(state, &tmp)) {
return NO;
}
if (tmp >= 0) {
result |= tmp << 7;
} else {
result |= (tmp & 0x7f) << 7;
if (!ReadRawByte(state, &tmp)) {
return NO;
}
if (tmp >= 0) {
result |= tmp << 14;
} else {
result |= (tmp & 0x7f) << 14;
if (!ReadRawByte(state, &tmp)) {
return NO;
}
if (tmp >= 0) {
result |= tmp << 21;
} else {
result |= (tmp & 0x7f) << 21;
if (!ReadRawByte(state, &tmp)) {
return NO;
}
result |= tmp << 28;
if (tmp < 0) {
// Discard upper 32 bits.
for (int i = 0; i < 5; ++i) {
if (!ReadRawByte(state, &tmp)) {
return NO;
}
if (tmp >= 0) {
*output = result;
return YES;
}
}
return NO;
}
}
}
}
*output = result;
return YES;
}
@interface FIRMessagingCodedInputStream()
@property(nonatomic, readwrite, strong) NSData *buffer;
@property(nonatomic, readwrite, assign) BufferState state;
@end
@implementation FIRMessagingCodedInputStream;
- (instancetype)initWithData:(NSData *)data {
self = [super init];
if (self) {
_buffer = data;
_state.bytes = _buffer.bytes;
_state.bufferSize = _buffer.length;
}
return self;
}
- (size_t)offset {
return _state.bufferPos;
}
- (BOOL)readTag:(int8_t *)tag {
return ReadRawByte(&_state, tag);
}
- (BOOL)readLength:(int32_t *)length {
return ReadRawVarInt32(&_state, length);
}
- (NSData *)readDataWithLength:(uint32_t)length {
if (!CheckSize(&_state, length)) {
return nil;
}
const void *bytesToRead = _state.bytes + _state.bufferPos;
NSData *result = [NSData dataWithBytes:bytesToRead length:length];
_state.bufferPos += length;
return result;
}
@end
@@ -0,0 +1,106 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
@class FIRMessagingConnection;
@class FIRMessagingDataMessageManager;
@class FIRMessagingRmqManager;
@class GtalkDataMessageStanza;
@class GPBMessage;
typedef void (^FIRMessagingMessageHandler)(NSDictionary *);
typedef NS_ENUM(NSUInteger, FIRMessagingConnectionState) {
kFIRMessagingConnectionNotConnected = 0,
kFIRMessagingConnectionConnecting,
kFIRMessagingConnectionConnected,
kFIRMessagingConnectionSignedIn,
};
typedef NS_ENUM(NSUInteger, FIRMessagingConnectionCloseReason) {
kFIRMessagingConnectionCloseReasonSocketDisconnected = 0,
kFIRMessagingConnectionCloseReasonTimeout,
kFIRMessagingConnectionCloseReasonUserDisconnect,
};
@protocol FIRMessagingConnectionDelegate<NSObject>
- (void)connection:(FIRMessagingConnection *)fcmConnection
didCloseForReason:(FIRMessagingConnectionCloseReason)reason;
- (void)didLoginWithConnection:(FIRMessagingConnection *)fcmConnection;
- (void)connectionDidRecieveMessage:(GtalkDataMessageStanza *)message;
/**
* Called when a stream ACK or a selective ACK are received - this indicates the
* message has been received by MCS.
* @return The count of rmqIds deleted from the client RMQ store.
*/
- (int)connectionDidReceiveAckForRmqIds:(NSArray *)rmqIds;
@end
/**
* This class maintains the actual FIRMessaging connection that we use to receive and send messages
* while the app is in foreground. Once we have a registrationID from the FIRMessaging backend we
* are able to set up this connection which is used for any further communication with FIRMessaging
* backend. In case the connection breaks off while the app is still being used we try to rebuild
* the connection with an exponential backoff.
*
* This class also notifies the delegate about the main events happening in the lifcycle of the
* FIRMessaging connection (read FIRMessagingConnectionDelegate). All of the `on-the-wire`
* interactions with FIRMessaging are channelled through here.
*/
@interface FIRMessagingConnection : NSObject
@property(nonatomic, readonly, assign) FIRMessagingConnectionState state;
@property(nonatomic, readonly, copy) NSString *host;
@property(nonatomic, readonly, assign) NSUInteger port;
@property(nonatomic, readwrite, weak) id<FIRMessagingConnectionDelegate> delegate;
- (instancetype)initWithAuthID:(NSString *)authId
token:(NSString *)token
host:(NSString *)host
port:(NSUInteger)port
runLoop:(NSRunLoop *)runLoop
rmq2Manager:(FIRMessagingRmqManager *)rmq2Manager
fcmManager:(FIRMessagingDataMessageManager *)dataMessageManager;
- (void)signIn; // connect
- (void)signOut; // disconnect
/**
* Teardown the FIRMessaging connection and deallocate the resources being held up by the
* connection.
*/
- (void)teardown;
/**
* Send proto to the wire. The message will be cached before we try to send so that in case of
* failure we can send it again later on when we have connection.
*/
- (void)sendProto:(GPBMessage *)proto;
/**
* Send a message after the currently in progress connection succeeds, otherwise drop it.
*
* This should be used for TTL=0 messages that force a reconnect. They shouldn't be persisted
* in the RMQ, but they should be sent if the reconnect is successful.
*/
- (void)sendOnConnectOrDrop:(GPBMessage *)message;
@end
@@ -0,0 +1,708 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessagingConnection.h"
#import "Protos/GtalkCore.pbobjc.h"
#import "Protos/GtalkExtensions.pbobjc.h"
#import "FIRMessaging.h"
#import "FIRMessagingDataMessageManager.h"
#import "FIRMessagingDefines.h"
#import "FIRMessagingLogger.h"
#import "FIRMessagingRmqManager.h"
#import "FIRMessagingSecureSocket.h"
#import "FIRMessagingUtilities.h"
#import "FIRMessagingVersionUtilities.h"
#import "FIRMessaging_Private.h"
static NSInteger const kIqSelectiveAck = 12;
static NSInteger const kIqStreamAck = 13;
static int const kInvalidStreamId = -1;
// Threshold for number of messages removed that we will ack, for short lived connections
static int const kMessageRemoveAckThresholdCount = 5;
static NSTimeInterval const kHeartbeatInterval = 30.0;
static NSTimeInterval const kConnectionTimeout = 20.0;
static int32_t const kAckingInterval = 10;
static NSString *const kUnackedS2dIdKey = @"FIRMessagingUnackedS2dIdKey";
static NSString *const kAckedS2dIdMapKey = @"FIRMessagingAckedS2dIdMapKey";
static NSString *const kRemoteFromAddress = @"from";
@interface FIRMessagingD2SInfo : NSObject
@property(nonatomic, readwrite, assign) int streamId;
@property(nonatomic, readwrite, strong) NSString *d2sID;
- (instancetype)initWithStreamId:(int)streamId d2sId:(NSString *)d2sID;
@end
@implementation FIRMessagingD2SInfo
- (instancetype)initWithStreamId:(int)streamId d2sId:(NSString *)d2sID {
self = [super init];
if (self) {
_streamId = streamId;
_d2sID = [d2sID copy];
}
return self;
}
- (BOOL)isEqual:(id)object {
if ([object isKindOfClass:[self class]]) {
FIRMessagingD2SInfo *other = (FIRMessagingD2SInfo *)object;
return self.streamId == other.streamId && [self.d2sID isEqualToString:other.d2sID];
}
return NO;
}
- (NSUInteger)hash {
return [self.d2sID hash];
}
@end
@interface FIRMessagingConnection ()<FIRMessagingSecureSocketDelegate>
@property(nonatomic, readwrite, weak) FIRMessagingRmqManager *rmq2Manager;
@property(nonatomic, readwrite, weak) FIRMessagingDataMessageManager *dataMessageManager;
@property(nonatomic, readwrite, assign) FIRMessagingConnectionState state;
@property(nonatomic, readwrite, copy) NSString *host;
@property(nonatomic, readwrite, assign) NSUInteger port;
@property(nonatomic, readwrite, strong) NSString *authId;
@property(nonatomic, readwrite, strong) NSString *token;
@property(nonatomic, readwrite, strong) FIRMessagingSecureSocket *socket;
@property(nonatomic, readwrite, assign) int64_t lastLoginServerTimestamp;
@property(nonatomic, readwrite, assign) int lastStreamIdAcked;
@property(nonatomic, readwrite, assign) int inStreamId;
@property(nonatomic, readwrite, assign) int outStreamId;
@property(nonatomic, readwrite, strong) NSMutableArray *unackedS2dIds;
@property(nonatomic, readwrite, strong) NSMutableDictionary *ackedS2dMap;
@property(nonatomic, readwrite, strong) NSMutableArray *d2sInfos;
// ttl=0 messages that need to be sent as soon as we establish a connection
@property(nonatomic, readwrite, strong) NSMutableArray *sendOnConnectMessages;
@property(nonatomic, readwrite, strong) NSRunLoop *runLoop;
@end
@implementation FIRMessagingConnection;
- (instancetype)initWithAuthID:(NSString *)authId
token:(NSString *)token
host:(NSString *)host
port:(NSUInteger)port
runLoop:(NSRunLoop *)runLoop
rmq2Manager:(FIRMessagingRmqManager *)rmq2Manager
fcmManager:(FIRMessagingDataMessageManager *)dataMessageManager {
self = [super init];
if (self) {
_authId = [authId copy];
_token = [token copy];
_host = [host copy];
_port = port;
_runLoop = runLoop;
_rmq2Manager = rmq2Manager;
_dataMessageManager = dataMessageManager;
_d2sInfos = [NSMutableArray array];
_unackedS2dIds = [NSMutableArray arrayWithArray:[_rmq2Manager unackedS2dRmqIds]];
_ackedS2dMap = [NSMutableDictionary dictionary];
_sendOnConnectMessages = [NSMutableArray array];
}
return self;
}
- (NSString *)description {
return [NSString stringWithFormat:@"host: %@, port: %lu, stream id in: %d, stream id out: %d",
self.host,
_FIRMessaging_UL(self.port),
self.inStreamId,
self.outStreamId];
}
- (void)signIn {
_FIRMessagingDevAssert(self.state == kFIRMessagingConnectionNotConnected, @"Invalid connection state.");
if (self.state != kFIRMessagingConnectionNotConnected) {
return;
}
// break it up for testing
[self setupConnectionSocket];
[self connectToSocket:self.socket];
}
- (void)setupConnectionSocket {
self.socket = [[FIRMessagingSecureSocket alloc] init];
self.socket.delegate = self;
}
- (void)connectToSocket:(FIRMessagingSecureSocket *)socket {
self.state = kFIRMessagingConnectionConnecting;
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection000,
@"Start connecting to FIRMessaging service.");
[socket connectToHost:self.host port:self.port onRunLoop:self.runLoop];
}
- (void)signOut {
// Clear the list of messages to be sent on connect. This will only
// have messages in it if an error happened before receiving the LoginResponse.
[self.sendOnConnectMessages removeAllObjects];
if (self.state == kFIRMessagingConnectionSignedIn) {
[self sendClose];
}
if (self.state != kFIRMessagingConnectionNotConnected) {
[self disconnect];
}
}
- (void)teardown {
if (self.state != kFIRMessagingConnectionNotConnected) {
[self disconnect];
}
}
#pragma mark - FIRMessagingSecureSocketDelegate
- (void)secureSocketDidConnect:(FIRMessagingSecureSocket *)socket {
self.state = kFIRMessagingConnectionConnected;
self.lastStreamIdAcked = 0;
self.inStreamId = 0;
self.outStreamId = 0;
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection001,
@"Connected to FIRMessaging service.");
[self resetUnconfirmedAcks];
[self sendLoginRequest:self.authId token:self.token];
}
- (void)didDisconnectWithSecureSocket:(FIRMessagingSecureSocket *)socket {
_FIRMessagingDevAssert(self.socket == socket, @"Invalid socket");
_FIRMessagingDevAssert(self.socket.state == kFIRMessagingSecureSocketClosed, @"Socket already closed");
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection002,
@"Secure socket disconnected from FIRMessaging service.");
[self disconnect];
[self.delegate connection:self didCloseForReason:kFIRMessagingConnectionCloseReasonSocketDisconnected];
}
- (void)secureSocket:(FIRMessagingSecureSocket *)socket
didReceiveData:(NSData *)data
withTag:(int8_t)tag {
if (tag < 0) {
// Invalid proto tag
return;
}
Class klassForTag = FIRMessagingGetClassForTag((FIRMessagingProtoTag)tag);
if ([klassForTag isSubclassOfClass:[NSNull class]]) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeConnection003, @"Invalid tag %d for proto",
tag);
return;
}
GPBMessage *proto = [klassForTag parseFromData:data error:NULL];
if (tag == kFIRMessagingProtoTagLoginResponse && self.state != kFIRMessagingConnectionConnected) {
FIRMessagingLoggerDebug(
kFIRMessagingMessageCodeConnection004,
@"Should not receive generated message when the connection is not connected.");
return;
} else if (tag != kFIRMessagingProtoTagLoginResponse && self.state != kFIRMessagingConnectionSignedIn) {
FIRMessagingLoggerDebug(
kFIRMessagingMessageCodeConnection005,
@"Should not receive generated message when the connection is not signed in.");
return;
}
// If traffic is received after a heartbeat it is safe to assume the connection is healthy.
[self cancelConnectionTimeoutTask];
[self performSelector:@selector(sendHeartbeatPing)
withObject:nil
afterDelay:kHeartbeatInterval];
[self willProcessProto:proto];
switch (tag) {
case kFIRMessagingProtoTagLoginResponse:
[self didReceiveLoginResponse:(GtalkLoginResponse *)proto];
break;
case kFIRMessagingProtoTagDataMessageStanza:
[self didReceiveDataMessageStanza:(GtalkDataMessageStanza *)proto];
break;
case kFIRMessagingProtoTagHeartbeatPing:
[self didReceiveHeartbeatPing:(GtalkHeartbeatPing *)proto];
break;
case kFIRMessagingProtoTagHeartbeatAck:
[self didReceiveHeartbeatAck:(GtalkHeartbeatAck *)proto];
break;
case kFIRMessagingProtoTagClose:
[self didReceiveClose:(GtalkClose *)proto];
break;
case kFIRMessagingProtoTagIqStanza:
[self handleIqStanza:(GtalkIqStanza *)proto];
break;
default:
[self didReceiveUnhandledProto:proto];
break;
}
}
// Called from secure socket once we have send the proto with given rmqId over the wire
// since we are mostly concerned with user facing messages which certainly have a rmqId
// we can retrieve them from the Rmq if necessary to look at stuff but for now we just
// log it.
- (void)secureSocket:(FIRMessagingSecureSocket *)socket
didSendProtoWithTag:(int8_t)tag
rmqId:(NSString *)rmqId {
// log the message
[self logMessage:rmqId messageType:tag isOut:YES];
}
#pragma mark - FIRMessagingTestConnection
- (void)sendProto:(GPBMessage *)proto {
FIRMessagingProtoTag tag = FIRMessagingGetTagForProto(proto);
if (tag == kFIRMessagingProtoTagLoginRequest && self.state != kFIRMessagingConnectionConnected) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection006,
@"Cannot send generated message when the connection is not connected.");
return;
} else if (tag != kFIRMessagingProtoTagLoginRequest && self.state != kFIRMessagingConnectionSignedIn) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection007,
@"Cannot send generated message when the connection is not signed in.");
return;
}
_FIRMessagingDevAssert(self.socket != nil, @"Socket shouldn't be nil");
if (self.socket == nil) {
return;
}
[self willSendProto:proto];
[self.socket sendData:proto.data withTag:tag rmqId:FIRMessagingGetRmq2Id(proto)];
}
- (void)sendOnConnectOrDrop:(GPBMessage *)message {
if (self.state == kFIRMessagingConnectionSignedIn) {
// If a connection has already been established, send normally
[self sendProto:message];
} else {
// Otherwise add them to the list of messages to send after login
[self.sendOnConnectMessages addObject:message];
}
}
+ (GtalkLoginRequest *)loginRequestWithToken:(NSString *)token authID:(NSString *)authID {
GtalkLoginRequest *login = [[GtalkLoginRequest alloc] init];
login.accountId = 1000000;
login.authService = GtalkLoginRequest_AuthService_AndroidId;
login.authToken = token;
login.id_p = [NSString stringWithFormat:@"%@-%@", @"ios", FIRMessagingCurrentLibraryVersion()];
login.domain = @"mcs.android.com";
login.deviceId = [NSString stringWithFormat:@"android-%llx", authID.longLongValue];
login.networkType = [self currentNetworkType];
login.resource = authID;
login.user = authID;
login.useRmq2 = YES;
login.lastRmqId = 1; // Sending not enabled yet so this stays as 1.
return login;
}
+ (int32_t)currentNetworkType {
// http://developer.android.com/reference/android/net/ConnectivityManager.html
int32_t fcmNetworkType;
FIRMessagingNetworkStatus type = [[FIRMessaging messaging] networkType];
switch (type) {
case kFIRMessagingReachabilityReachableViaWiFi:
fcmNetworkType = 1;
break;
case kFIRMessagingReachabilityReachableViaWWAN:
fcmNetworkType = 0;
break;
default:
fcmNetworkType = -1;
break;
}
return fcmNetworkType;
}
- (void)sendLoginRequest:(NSString *)authId
token:(NSString *)token {
GtalkLoginRequest *login = [[self class] loginRequestWithToken:token authID:authId];
// clear the messages sent during last connection
if ([self.d2sInfos count]) {
[self.d2sInfos removeAllObjects];
}
if (self.unackedS2dIds.count > 0) {
FIRMessagingLoggerDebug(
kFIRMessagingMessageCodeConnection008,
@"There are unacked persistent Ids in the login request: %@",
[self.unackedS2dIds.description stringByReplacingOccurrencesOfString:@"%"
withString:@"%%"]);
}
// Send out acks.
for (NSString *unackedPersistentS2dId in self.unackedS2dIds) {
[login.receivedPersistentIdArray addObject:unackedPersistentS2dId];
}
GtalkSetting *setting = [[GtalkSetting alloc] init];
setting.name = @"new_vc";
setting.value = @"1";
[login.settingArray addObject:setting];
[self sendProto:login];
}
- (void)sendHeartbeatAck {
[self sendProto:[[GtalkHeartbeatAck alloc] init]];
}
- (void)sendHeartbeatPing {
// cancel the previous heartbeat request.
[NSObject cancelPreviousPerformRequestsWithTarget:self
selector:@selector(sendHeartbeatPing)
object:nil];
[self scheduleConnectionTimeoutTask];
[self sendProto:[[GtalkHeartbeatPing alloc] init]];
}
+ (GtalkIqStanza *)createStreamAck {
GtalkIqStanza *iq = [[GtalkIqStanza alloc] init];
iq.type = GtalkIqStanza_IqType_Set;
iq.id_p = @"";
GtalkExtension *ext = [[GtalkExtension alloc] init];
ext.id_p = kIqStreamAck;
ext.data_p = @"";
iq.extension = ext;
return iq;
}
- (void)sendStreamAck {
GtalkIqStanza *iq = [[self class] createStreamAck];
[self sendProto:iq];
}
- (void)sendClose {
[self sendProto:[[GtalkClose alloc] init]];
}
- (void)handleIqStanza:(GtalkIqStanza *)iq {
if (iq.hasExtension) {
if (iq.extension.id_p == kIqStreamAck) {
[self didReceiveStreamAck:iq];
return;
}
if (iq.extension.id_p == kIqSelectiveAck) {
[self didReceiveSelectiveAck:iq];
return;
}
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection009, @"Unknown ack extension id %d.",
iq.extension.id_p);
} else {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection010, @"Ip stanza without extension.");
}
[self didReceiveUnhandledProto:iq];
}
- (void)didReceiveLoginResponse:(GtalkLoginResponse *)loginResponse {
if (loginResponse.hasError) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection011,
@"Login error with type: %@, message: %@.", loginResponse.error.type,
loginResponse.error.message);
return;
}
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection012, @"Logged onto MCS service.");
// We sent the persisted list of unack'd messages with login so we can assume they have been ack'd
// by the server.
_FIRMessagingDevAssert(self.unackedS2dIds.count == 0, @"No ids present");
_FIRMessagingDevAssert(self.outStreamId == 1, @"Login should be the first stream id");
self.state = kFIRMessagingConnectionSignedIn;
self.lastLoginServerTimestamp = loginResponse.serverTimestamp;
[self.delegate didLoginWithConnection:self];
[self sendHeartbeatPing];
// Add all the TTL=0 messages on connect
for (GPBMessage *message in self.sendOnConnectMessages) {
[self sendProto:message];
}
[self.sendOnConnectMessages removeAllObjects];
}
- (void)didReceiveHeartbeatPing:(GtalkHeartbeatPing *)heartbeatPing {
[self sendHeartbeatAck];
}
- (void)didReceiveHeartbeatAck:(GtalkHeartbeatAck *)heartbeatAck {
}
- (void)didReceiveDataMessageStanza:(GtalkDataMessageStanza *)dataMessageStanza {
// TODO: Maybe add support raw data later
[self.delegate connectionDidRecieveMessage:dataMessageStanza];
}
- (void)didReceiveUnhandledProto:(GPBMessage *)proto {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection013, @"Received unhandled proto");
}
- (void)didReceiveStreamAck:(GtalkIqStanza *)iq {
// Server received some stuff from us we don't really need to do anything special
}
- (void)didReceiveSelectiveAck:(GtalkIqStanza *)iq {
GtalkExtension *extension = iq.extension;
if (extension) {
int extensionId = extension.id_p;
if (extensionId == kIqSelectiveAck) {
NSString *dataString = extension.data_p;
GtalkSelectiveAck *selectiveAck = [[GtalkSelectiveAck alloc] init];
[selectiveAck mergeFromData:[dataString dataUsingEncoding:NSUTF8StringEncoding]
extensionRegistry:nil];
NSArray <NSString *>*acks = [selectiveAck idArray];
// we've received ACK's
[self.delegate connectionDidReceiveAckForRmqIds:acks];
// resend unacked messages
[self.dataMessageManager resendMessagesWithConnection:self];
}
}
}
- (void)didReceiveClose:(GtalkClose *)close {
[self disconnect];
}
- (void)willProcessProto:(GPBMessage *)proto {
self.inStreamId++;
if ([proto isKindOfClass:GtalkDataMessageStanza.class]) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection014,
@"RMQ: Receiving %@ with rmq_id: %@ incoming stream Id: %d",
proto.class, FIRMessagingGetRmq2Id(proto), self.inStreamId);
} else {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection015,
@"RMQ: Receiving %@ with incoming stream Id: %d.", proto.class,
self.inStreamId);
}
int streamId = FIRMessagingGetLastStreamId(proto);
if (streamId != kInvalidStreamId) {
// confirm the D2S messages that were sent by us
[self confirmAckedD2sIdsWithStreamId:streamId];
// We can now confirm that our ack was received by the server and start our unack'd list fresh
// with the proto we just received.
[self confirmAckedS2dIdsWithStreamId:streamId];
}
NSString *rmq2Id = FIRMessagingGetRmq2Id(proto);
if (rmq2Id != nil) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection016,
@"RMQ: Add unacked persistent Id: %@.",
[rmq2Id stringByReplacingOccurrencesOfString:@"%" withString:@"%%"]);
[self.unackedS2dIds addObject:rmq2Id];
[self.rmq2Manager saveS2dMessageWithRmqId:rmq2Id]; // RMQ save
}
BOOL explicitAck = ([proto isKindOfClass:[GtalkDataMessageStanza class]] &&
[(GtalkDataMessageStanza *)proto immediateAck]);
// If we have not sent anything and the ack threshold has been reached then explicitly send one
// to notify the server that we have received messages.
if (self.inStreamId - self.lastStreamIdAcked >= kAckingInterval || explicitAck) {
[self sendStreamAck];
}
}
- (void)willSendProto:(GPBMessage *)proto {
self.outStreamId++;
NSString *rmq2Id = FIRMessagingGetRmq2Id(proto);
if ([rmq2Id length]) {
FIRMessagingD2SInfo *d2sInfo = [[FIRMessagingD2SInfo alloc] initWithStreamId:self.outStreamId d2sId:rmq2Id];
[self.d2sInfos addObject:d2sInfo];
}
// each time we send a d2s message, it acks previously received
// s2d messages via the last (s2d) stream id received.
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection017,
@"RMQ: Sending %@ with outgoing stream Id: %d.", proto.class,
self.outStreamId);
// We have received messages since last time we sent something - send ack info to server.
if (self.inStreamId > self.lastStreamIdAcked) {
FIRMessagingSetLastStreamId(proto, self.inStreamId);
self.lastStreamIdAcked = self.inStreamId;
}
if (self.unackedS2dIds.count > 0) {
// Move all 'unack'd' messages to the ack'd map so they can be removed once the
// ack is confirmed.
NSArray *ackedS2dIds = [NSArray arrayWithArray:self.unackedS2dIds];
FIRMessagingLoggerDebug(
kFIRMessagingMessageCodeConnection018, @"RMQ: Mark persistent Ids as acked: %@.",
[ackedS2dIds.description stringByReplacingOccurrencesOfString:@"%" withString:@"%%"]);
[self.unackedS2dIds removeAllObjects];
self.ackedS2dMap[[@(self.outStreamId) stringValue]] = ackedS2dIds;
}
}
#pragma mark - Private
/**
* This processes the s2d message received in reference to the d2s messages
* that we have sent before.
*/
- (void)confirmAckedD2sIdsWithStreamId:(int)lastReceivedStreamId {
NSMutableArray *d2sIdsAcked = [NSMutableArray array];
for (FIRMessagingD2SInfo *d2sInfo in self.d2sInfos) {
if (lastReceivedStreamId < d2sInfo.streamId) {
break;
}
[d2sIdsAcked addObject:d2sInfo];
}
NSMutableArray *rmqIds = [NSMutableArray arrayWithCapacity:[d2sIdsAcked count]];
// remove ACK'ed messages
for (FIRMessagingD2SInfo *d2sInfo in d2sIdsAcked) {
if ([d2sInfo.d2sID length]) {
[rmqIds addObject:d2sInfo.d2sID];
}
[self.d2sInfos removeObject:d2sInfo];
}
[self.delegate connectionDidReceiveAckForRmqIds:rmqIds];
int count = [self.delegate connectionDidReceiveAckForRmqIds:rmqIds];
if (kMessageRemoveAckThresholdCount > 0 && count >= kMessageRemoveAckThresholdCount) {
// For short lived connections, if a large number of messages are removed, send an
// ack straight away so the server knows that this message was received.
[self sendStreamAck];
}
}
/**
* Called when a stream ACK or a selective ACK are received - this indicates the message has
* been received by MCS.
*/
- (void)didReceiveAckForRmqIds:(NSArray *)rmqIds {
// TODO: let the user know that the following messages were received by the server
}
- (void)confirmAckedS2dIdsWithStreamId:(int)lastReceivedStreamId {
// If the server hasn't received the streamId yet.
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection019,
@"RMQ: Server last received stream Id: %d.", lastReceivedStreamId);
if (lastReceivedStreamId < self.outStreamId) {
// TODO: This could be a good indicator that we need to re-send something (acks)?
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection020,
@"RMQ: There are unsent messages that should be send...\n"
"server received: %d\nlast stream id sent: %d",
lastReceivedStreamId, self.outStreamId);
}
NSSet *ackedStreamIds =
[self.ackedS2dMap keysOfEntriesPassingTest:^BOOL(id key, id obj, BOOL *stop) {
NSString *streamId = key;
return streamId.intValue <= lastReceivedStreamId;
}];
NSMutableArray *s2dIdsToDelete = [NSMutableArray array];
for (NSString *streamId in ackedStreamIds) {
NSArray *ackedS2dIds = self.ackedS2dMap[streamId];
if (ackedS2dIds.count > 0) {
FIRMessagingLoggerDebug(
kFIRMessagingMessageCodeConnection021,
@"RMQ: Mark persistent Ids as confirmed by stream id %@: %@.", streamId,
[ackedS2dIds.description stringByReplacingOccurrencesOfString:@"%" withString:@"%%"]);
[self.ackedS2dMap removeObjectForKey:streamId];
}
[s2dIdsToDelete addObjectsFromArray:ackedS2dIds];
}
// clean up s2d ids that the server knows we've received.
// we let the server know via a s2d last stream id received in a
// d2s message. the server lets us know it has received our d2s
// message via a d2s last stream id received in a s2d message.
[self.rmq2Manager removeS2dIds:s2dIdsToDelete];
}
- (void)resetUnconfirmedAcks {
[self.ackedS2dMap enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
[self.unackedS2dIds addObjectsFromArray:obj];
}];
[self.ackedS2dMap removeAllObjects];
}
- (void)disconnect {
_FIRMessagingDevAssert(self.state != kFIRMessagingConnectionNotConnected, @"Connection already not connected");
// cancel pending timeout tasks.
[self cancelConnectionTimeoutTask];
// cancel pending heartbeat.
[NSObject cancelPreviousPerformRequestsWithTarget:self
selector:@selector(sendHeartbeatPing)
object:nil];
// Unset the delegate. FIRMessagingConnection will not receive further events from the socket from now on.
self.socket.delegate = nil;
[self.socket disconnect];
self.state = kFIRMessagingConnectionNotConnected;
}
- (void)connectionTimedOut {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection022,
@"Connection to FIRMessaging service timed out.");
[self disconnect];
[self.delegate connection:self didCloseForReason:kFIRMessagingConnectionCloseReasonTimeout];
}
- (void)scheduleConnectionTimeoutTask {
// cancel the previous heartbeat timeout event and schedule a new one.
[self cancelConnectionTimeoutTask];
[self performSelector:@selector(connectionTimedOut)
withObject:nil
afterDelay:[self connectionTimeoutInterval]];
}
- (void)cancelConnectionTimeoutTask {
// cancel pending timeout tasks.
[NSObject cancelPreviousPerformRequestsWithTarget:self
selector:@selector(connectionTimedOut)
object:nil];
}
- (void)logMessage:(NSString *)description messageType:(int)messageType isOut:(BOOL)isOut {
messageType = isOut ? -messageType : messageType;
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeConnection023,
@"Send msg: %@ type: %d inStreamId: %d outStreamId: %d", description,
messageType, self.inStreamId, self.outStreamId);
}
- (NSTimeInterval)connectionTimeoutInterval {
return kConnectionTimeout;
}
@end
@@ -0,0 +1,58 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Global constants to be put here.
*
*/
#import <Foundation/Foundation.h>
#ifndef _FIRMessaging_CONSTANTS_H
#define _FIRMessaging_CONSTANTS_H
FOUNDATION_EXPORT NSString *const kFIRMessagingRawDataKey;
FOUNDATION_EXPORT NSString *const kFIRMessagingCollapseKey;
FOUNDATION_EXPORT NSString *const kFIRMessagingFromKey;
FOUNDATION_EXPORT NSString *const kFIRMessagingSendTo;
FOUNDATION_EXPORT NSString *const kFIRMessagingSendTTL;
FOUNDATION_EXPORT NSString *const kFIRMessagingSendDelay;
FOUNDATION_EXPORT NSString *const kFIRMessagingSendMessageID;
FOUNDATION_EXPORT NSString *const KFIRMessagingSendMessageAppData;
FOUNDATION_EXPORT NSString *const kFIRMessagingMessageInternalReservedKeyword;
FOUNDATION_EXPORT NSString *const kFIRMessagingMessagePersistentIDKey;
FOUNDATION_EXPORT NSString *const kFIRMessagingMessageIDKey;
FOUNDATION_EXPORT NSString *const kFIRMessagingMessageAPNSContentAvailableKey;
FOUNDATION_EXPORT NSString *const kFIRMessagingMessageSyncViaMCSKey;
FOUNDATION_EXPORT NSString *const kFIRMessagingMessageSyncMessageTTLKey;
FOUNDATION_EXPORT NSString *const kFIRMessagingMessageLinkKey;
FOUNDATION_EXPORT NSString *const kFIRMessagingRemoteNotificationsProxyEnabledInfoPlistKey;
FOUNDATION_EXPORT NSString *const kFIRMessagingSubDirectoryName;
// Notifications
FOUNDATION_EXPORT NSString *const kFIRMessagingCheckinFetchedNotification;
FOUNDATION_EXPORT NSString *const kFIRMessagingAPNSTokenNotification;
FOUNDATION_EXPORT NSString *const kFIRMessagingFCMTokenNotification;
FOUNDATION_EXPORT NSString *const kFIRMessagingInstanceIDTokenRefreshNotification __deprecated_msg("Use kFIRMessagingRegistrationTokenRefreshNotification instead");
FOUNDATION_EXPORT NSString *const kFIRMessagingRegistrationTokenRefreshNotification;
FOUNDATION_EXPORT const int kFIRMessagingSendTtlDefault; // 24 hours
#endif
@@ -0,0 +1,52 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessagingConstants.h"
NSString *const kFIRMessagingRawDataKey = @"rawData";
NSString *const kFIRMessagingCollapseKey = @"collapse_key";
NSString *const kFIRMessagingFromKey = @"from";
NSString *const kFIRMessagingSendTo = @"google." @"to";
NSString *const kFIRMessagingSendTTL = @"google." @"ttl";
NSString *const kFIRMessagingSendDelay = @"google." @"delay";
NSString *const kFIRMessagingSendMessageID = @"google." @"msg_id";
NSString *const KFIRMessagingSendMessageAppData = @"google." @"data";
NSString *const kFIRMessagingMessageInternalReservedKeyword = @"gcm.";
NSString *const kFIRMessagingMessagePersistentIDKey = @"persistent_id";
NSString *const kFIRMessagingMessageIDKey = @"gcm." @"message_id";
NSString *const kFIRMessagingMessageAPNSContentAvailableKey = @"content-available";
NSString *const kFIRMessagingMessageSyncViaMCSKey = @"gcm." @"duplex";
NSString *const kFIRMessagingMessageSyncMessageTTLKey = @"gcm." @"ttl";
NSString *const kFIRMessagingMessageLinkKey = @"gcm." @"app_link";
NSString *const kFIRMessagingRemoteNotificationsProxyEnabledInfoPlistKey =
@"FirebaseAppDelegateProxyEnabled";
NSString *const kFIRMessagingSubDirectoryName = @"Google/FirebaseMessaging";
// Notifications
NSString *const kFIRMessagingCheckinFetchedNotification = @"com.google.gcm.notif-checkin-fetched";
NSString *const kFIRMessagingAPNSTokenNotification = @"com.firebase.iid.notif.apns-token";
NSString *const kFIRMessagingFCMTokenNotification = @"com.firebase.iid.notif.fcm-token";
NSString *const kFIRMessagingInstanceIDTokenRefreshNotification =
@"com.firebase.iid.notif.refresh-token";
NSString *const kFIRMessagingRegistrationTokenRefreshNotification =
@"com.firebase.iid.notif.refresh-token";
const int kFIRMessagingSendTtlDefault = 24 * 60 * 60; // 24 hours
@@ -0,0 +1,44 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
FOUNDATION_EXPORT NSString *const kFIRMessagingContextManagerCategory;
FOUNDATION_EXPORT NSString *const kFIRMessagingContextManagerLocalTimeStart;
FOUNDATION_EXPORT NSString *const kFIRMessagingContextManagerLocalTimeEnd;
FOUNDATION_EXPORT NSString *const kFIRMessagingContextManagerBodyKey;
@interface FIRMessagingContextManagerService : NSObject
/**
* Check if the message is a context manager message or not.
*
* @param message The message to verify.
*
* @return YES if the message is a context manager message else NO.
*/
+ (BOOL)isContextManagerMessage:(NSDictionary *)message;
/**
* Handle context manager message.
*
* @param message The message to handle.
*
* @return YES if the message was handled successfully else NO.
*/
+ (BOOL)handleContextManagerMessage:(NSDictionary *)message;
@end
@@ -0,0 +1,202 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessagingContextManagerService.h"
#import "FIRMessagingDefines.h"
#import "FIRMessagingLogger.h"
#import "FIRMessagingUtilities.h"
#import <GoogleUtilities/GULAppDelegateSwizzler.h>
#define kFIRMessagingContextManagerPrefixKey @"google.c.cm."
#define kFIRMessagingContextManagerNotificationKeyPrefix @"gcm.notification."
static NSString *const kLogTag = @"FIRMessagingAnalytics";
static NSString *const kLocalTimeFormatString = @"yyyy-MM-dd HH:mm:ss";
static NSString *const kContextManagerPrefixKey = kFIRMessagingContextManagerPrefixKey;
// Local timed messages (format yyyy-mm-dd HH:mm:ss)
NSString *const kFIRMessagingContextManagerLocalTimeStart = kFIRMessagingContextManagerPrefixKey @"lt_start";
NSString *const kFIRMessagingContextManagerLocalTimeEnd = kFIRMessagingContextManagerPrefixKey @"lt_end";
// Local Notification Params
NSString *const kFIRMessagingContextManagerBodyKey = kFIRMessagingContextManagerNotificationKeyPrefix @"body";
NSString *const kFIRMessagingContextManagerTitleKey = kFIRMessagingContextManagerNotificationKeyPrefix @"title";
NSString *const kFIRMessagingContextManagerBadgeKey = kFIRMessagingContextManagerNotificationKeyPrefix @"badge";
NSString *const kFIRMessagingContextManagerCategoryKey =
kFIRMessagingContextManagerNotificationKeyPrefix @"click_action";
NSString *const kFIRMessagingContextManagerSoundKey = kFIRMessagingContextManagerNotificationKeyPrefix @"sound";
NSString *const kFIRMessagingContextManagerContentAvailableKey =
kFIRMessagingContextManagerNotificationKeyPrefix @"content-available";
typedef NS_ENUM(NSUInteger, FIRMessagingContextManagerMessageType) {
FIRMessagingContextManagerMessageTypeNone,
FIRMessagingContextManagerMessageTypeLocalTime,
};
@implementation FIRMessagingContextManagerService
+ (BOOL)isContextManagerMessage:(NSDictionary *)message {
// For now we only support local time in ContextManager.
if (![message[kFIRMessagingContextManagerLocalTimeStart] length]) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeContextManagerService000,
@"Received message missing local start time, dropped.");
return NO;
}
return YES;
}
+ (BOOL)handleContextManagerMessage:(NSDictionary *)message {
NSString *startTimeString = message[kFIRMessagingContextManagerLocalTimeStart];
if (startTimeString.length) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeContextManagerService001,
@"%@ Received context manager message with local time %@", kLogTag,
startTimeString);
return [self handleContextManagerLocalTimeMessage:message];
}
return NO;
}
+ (BOOL)handleContextManagerLocalTimeMessage:(NSDictionary *)message {
NSString *startTimeString = message[kFIRMessagingContextManagerLocalTimeStart];
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
[dateFormatter setDateFormat:kLocalTimeFormatString];
NSDate *startDate = [dateFormatter dateFromString:startTimeString];
_FIRMessagingDevAssert(startDate, @"Invalid local start date format %@", startTimeString);
if (!startTimeString) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeContextManagerService002,
@"Invalid local start date format %@. Message dropped",
startTimeString);
return NO;
}
NSDate *currentDate = [NSDate date];
if ([currentDate compare:startDate] == NSOrderedAscending) {
[self scheduleLocalNotificationForMessage:message
atDate:startDate];
} else {
// check end time has not passed
NSString *endTimeString = message[kFIRMessagingContextManagerLocalTimeEnd];
if (!endTimeString) {
FIRMessagingLoggerInfo(
kFIRMessagingMessageCodeContextManagerService003,
@"No end date specified for message, start date elapsed. Message dropped.");
return YES;
}
NSDate *endDate = [dateFormatter dateFromString:endTimeString];
_FIRMessagingDevAssert(endDate, @"Invalid local end date format %@", endTimeString);
if (!endTimeString) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeContextManagerService004,
@"Invalid local end date format %@. Message dropped", endTimeString);
return NO;
}
if ([endDate compare:currentDate] == NSOrderedAscending) {
// end date has already passed drop the message
FIRMessagingLoggerInfo(kFIRMessagingMessageCodeContextManagerService005,
@"End date %@ has already passed. Message dropped.", endTimeString);
return YES;
}
// schedule message right now (buffer 10s)
[self scheduleLocalNotificationForMessage:message
atDate:[currentDate dateByAddingTimeInterval:10]];
}
return YES;
}
+ (void)scheduleLocalNotificationForMessage:(NSDictionary *)message
atDate:(NSDate *)date {
#if TARGET_OS_IOS
NSDictionary *apsDictionary = message;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
UILocalNotification *notification = [[UILocalNotification alloc] init];
#pragma clang diagnostic pop
// A great way to understand timezones and UILocalNotifications
// http://stackoverflow.com/questions/18424569/understanding-uilocalnotification-timezone
notification.timeZone = [NSTimeZone defaultTimeZone];
notification.fireDate = date;
// In the current solution all of the display stuff goes into a special "aps" dictionary
// being sent in the message.
if ([apsDictionary[kFIRMessagingContextManagerBodyKey] length]) {
notification.alertBody = apsDictionary[kFIRMessagingContextManagerBodyKey];
}
if ([apsDictionary[kFIRMessagingContextManagerTitleKey] length]) {
// |alertTitle| is iOS 8.2+, so check if we can set it
if ([notification respondsToSelector:@selector(setAlertTitle:)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
notification.alertTitle = apsDictionary[kFIRMessagingContextManagerTitleKey];
#pragma pop
}
}
if (apsDictionary[kFIRMessagingContextManagerSoundKey]) {
notification.soundName = apsDictionary[kFIRMessagingContextManagerSoundKey];
}
if (apsDictionary[kFIRMessagingContextManagerBadgeKey]) {
notification.applicationIconBadgeNumber =
[apsDictionary[kFIRMessagingContextManagerBadgeKey] integerValue];
}
if (apsDictionary[kFIRMessagingContextManagerCategoryKey]) {
// |category| is iOS 8.0+, so check if we can set it
if ([notification respondsToSelector:@selector(setCategory:)]) {
notification.category = apsDictionary[kFIRMessagingContextManagerCategoryKey];
}
}
NSDictionary *userInfo = [self parseDataFromMessage:message];
if (userInfo.count) {
notification.userInfo = userInfo;
}
UIApplication *application = [GULAppDelegateSwizzler sharedApplication];
if (!application) {
return;
}
[application scheduleLocalNotification:notification];
#endif
}
+ (NSDictionary *)parseDataFromMessage:(NSDictionary *)message {
NSMutableDictionary *data = [NSMutableDictionary dictionary];
for (NSObject<NSCopying> *key in message) {
if ([key isKindOfClass:[NSString class]]) {
NSString *keyString = (NSString *)key;
if ([keyString isEqualToString:kFIRMessagingContextManagerContentAvailableKey]) {
continue;
} else if ([keyString hasPrefix:kContextManagerPrefixKey]) {
continue;
}
}
data[[key copy]] = message[key];
}
return [data copy];
}
@end
@@ -0,0 +1,101 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
@class GtalkDataMessageStanza;
@class FIRMessagingClient;
@class FIRMessagingConnection;
@class FIRMessagingReceiver;
@class FIRMessagingRmqManager;
@class FIRMessagingSyncMessageManager;
@protocol FIRMessagingDataMessageManagerDelegate <NSObject>
#pragma mark - Downstream Callbacks
/**
* Invoked when FIRMessaging receives a downstream message via the MCS connection.
* Let's the user know that they have received a new message by invoking the
* App's remoteNotification callback.
*
* @param message The downstream message received by the MCS connection.
*/
- (void)didReceiveMessage:(nonnull NSDictionary *)message
withIdentifier:(nullable NSString *)messageID;
#pragma mark - Upstream Callbacks
/**
* Notify the app that FIRMessaging will soon be sending the upstream message requested by the app.
*
* @param messageID The messageId passed in by the app to track this particular message.
* @param error The error in case FIRMessaging cannot send the message upstream.
*/
- (void)willSendDataMessageWithID:(nullable NSString *)messageID error:(nullable NSError *)error;
/**
* Notify the app that FIRMessaging did successfully send it's message via the MCS
* connection and the message was successfully delivered.
*
* @param messageId The messageId passed in by the app to track this particular
* message.
*/
- (void)didSendDataMessageWithID:(nonnull NSString *)messageId;
#pragma mark - Server Callbacks
/**
* Notify the app that FIRMessaging server deleted some messages which exceeded storage limits. This
* indicates the "deleted_messages" message type we received from the server.
*/
- (void)didDeleteMessagesOnServer;
@end
/**
* This manages all of the data messages being sent by the client and also the messages that
* were received from the server.
*/
@interface FIRMessagingDataMessageManager : NSObject
NS_ASSUME_NONNULL_BEGIN
- (instancetype)initWithDelegate:(id<FIRMessagingDataMessageManagerDelegate>)delegate
client:(FIRMessagingClient *)client
rmq2Manager:(FIRMessagingRmqManager *)rmq2Manager
syncMessageManager:(FIRMessagingSyncMessageManager *)syncMessageManager;
- (void)setDeviceAuthID:(NSString *)deviceAuthID secretToken:(NSString *)secretToken;
- (void)refreshDelayedMessages;
#pragma mark - Receive
- (nullable NSDictionary *)processPacket:(GtalkDataMessageStanza *)packet;
- (void)didReceiveParsedMessage:(NSDictionary *)message;
#pragma mark - Send
- (void)sendDataMessageStanza:(NSMutableDictionary *)dataMessage;
- (void)didSendDataMessageStanza:(GtalkDataMessageStanza *)message;
- (void)resendMessagesWithConnection:(FIRMessagingConnection *)connection;
NS_ASSUME_NONNULL_END
@end
@@ -0,0 +1,528 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessagingDataMessageManager.h"
#import "Protos/GtalkCore.pbobjc.h"
#import "FIRMessagingClient.h"
#import "FIRMessagingConnection.h"
#import "FIRMessagingConstants.h"
#import "FIRMessagingDefines.h"
#import "FIRMessagingDelayedMessageQueue.h"
#import "FIRMessagingLogger.h"
#import "FIRMessagingReceiver.h"
#import "FIRMessagingRmqManager.h"
#import "FIRMessaging_Private.h"
#import "FIRMessagingSyncMessageManager.h"
#import "FIRMessagingUtilities.h"
#import "NSError+FIRMessaging.h"
static const int kMaxAppDataSizeDefault = 4 * 1024; // 4k
static const int kMinDelaySeconds = 1; // 1 second
static const int kMaxDelaySeconds = 60 * 60; // 1 hour
static NSString *const kFromForFIRMessagingMessages = @"mcs.android.com";
static NSString *const kGSFMessageCategory = @"com.google.android.gsf.gtalkservice";
// TODO: Update Gcm to FIRMessaging in the constants below
static NSString *const kFCMMessageCategory = @"com.google.gcm";
static NSString *const kMessageReservedPrefix = @"google.";
static NSString *const kFCMMessageSpecialMessage = @"message_type";
// special messages sent by the server
static NSString *const kFCMMessageTypeDeletedMessages = @"deleted_messages";
static NSString *const kMCSNotificationPrefix = @"gcm.notification.";
static NSString *const kDataMessageNotificationKey = @"notification";
typedef NS_ENUM(int8_t, UpstreamForceReconnect) {
// Never force reconnect on upstream messages
kUpstreamForceReconnectOff = 0,
// Force reconnect for TTL=0 upstream messages
kUpstreamForceReconnectTTL0 = 1,
// Force reconnect for all upstream messages
kUpstreamForceReconnectAll = 2,
};
@interface FIRMessagingDataMessageManager ()
@property(nonatomic, readwrite, weak) FIRMessagingClient *client;
@property(nonatomic, readwrite, weak) FIRMessagingRmqManager *rmq2Manager;
@property(nonatomic, readwrite, weak) FIRMessagingSyncMessageManager *syncMessageManager;
@property(nonatomic, readwrite, weak) id<FIRMessagingDataMessageManagerDelegate> delegate;
@property(nonatomic, readwrite, strong) FIRMessagingDelayedMessageQueue *delayedMessagesQueue;
@property(nonatomic, readwrite, assign) int ttl;
@property(nonatomic, readwrite, copy) NSString *deviceAuthID;
@property(nonatomic, readwrite, copy) NSString *secretToken;
@property(nonatomic, readwrite, assign) int maxAppDataSize;
@property(nonatomic, readwrite, assign) UpstreamForceReconnect upstreamForceReconnect;
@end
@implementation FIRMessagingDataMessageManager
- (instancetype)initWithDelegate:(id<FIRMessagingDataMessageManagerDelegate>)delegate
client:(FIRMessagingClient *)client
rmq2Manager:(FIRMessagingRmqManager *)rmq2Manager
syncMessageManager:(FIRMessagingSyncMessageManager *)syncMessageManager {
self = [super init];
if (self) {
_delegate = delegate;
_client = client;
_rmq2Manager = rmq2Manager;
_syncMessageManager = syncMessageManager;
_ttl = kFIRMessagingSendTtlDefault;
_maxAppDataSize = kMaxAppDataSizeDefault;
// on by default
_upstreamForceReconnect = kUpstreamForceReconnectAll;
}
return self;
}
- (void)setDeviceAuthID:(NSString *)deviceAuthID secretToken:(NSString *)secretToken {
_FIRMessagingDevAssert([deviceAuthID length] && [secretToken length],
@"Invalid credentials for FIRMessaging");
self.deviceAuthID = deviceAuthID;
self.secretToken = secretToken;
}
- (void)refreshDelayedMessages {
FIRMessaging_WEAKIFY(self);
self.delayedMessagesQueue =
[[FIRMessagingDelayedMessageQueue alloc] initWithRmqScanner:self.rmq2Manager
sendDelayedMessagesHandler:^(NSArray *messages) {
FIRMessaging_STRONGIFY(self);
[self sendDelayedMessages:messages];
}];
}
- (nullable NSDictionary *)processPacket:(GtalkDataMessageStanza *)dataMessage {
NSString *category = dataMessage.category;
NSString *from = dataMessage.from;
if ([kFCMMessageCategory isEqualToString:category] ||
[kGSFMessageCategory isEqualToString:category]) {
[self handleMCSDataMessage:dataMessage];
return nil;
} else if ([kFromForFIRMessagingMessages isEqualToString:from]) {
[self handleMCSDataMessage:dataMessage];
return nil;
}
return [self parseDataMessage:dataMessage];
}
- (void)handleMCSDataMessage:(GtalkDataMessageStanza *)dataMessage {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager000,
@"Received message for FIRMessaging from downstream %@", dataMessage);
}
- (NSDictionary *)parseDataMessage:(GtalkDataMessageStanza *)dataMessage {
NSMutableDictionary *message = [NSMutableDictionary dictionary];
NSString *from = [dataMessage from];
if ([from length]) {
message[kFIRMessagingFromKey] = from;
}
// raw data
NSData *rawData = [dataMessage rawData];
if ([rawData length]) {
message[kFIRMessagingRawDataKey] = rawData;
}
NSString *token = [dataMessage token];
if ([token length]) {
message[kFIRMessagingCollapseKey] = token;
}
// Add the persistent_id. This would be removed later before sending the message to the device.
NSString *persistentID = [dataMessage persistentId];
_FIRMessagingDevAssert([persistentID length], @"Invalid MCS message without persistentID");
if ([persistentID length]) {
message[kFIRMessagingMessageIDKey] = persistentID;
}
// third-party data
for (GtalkAppData *item in dataMessage.appDataArray) {
_FIRMessagingDevAssert(item.hasKey && item.hasValue, @"Invalid AppData");
// do not process the "from" key -- is not useful
if ([kFIRMessagingFromKey isEqualToString:item.key]) {
continue;
}
// Filter the "gcm.notification." keys in the message
if ([item.key hasPrefix:kMCSNotificationPrefix]) {
NSString *key = [item.key substringFromIndex:[kMCSNotificationPrefix length]];
if ([key length]) {
if (!message[kDataMessageNotificationKey]) {
message[kDataMessageNotificationKey] = [NSMutableDictionary dictionary];
}
message[kDataMessageNotificationKey][key] = item.value;
} else {
_FIRMessagingDevAssert([key length], @"Invalid key in MCS message: %@", key);
FIRMessagingLoggerError(kFIRMessagingMessageCodeDataMessageManager001,
@"Invalid key in MCS message: %@", key);
}
continue;
}
// Filter the "gcm.duplex" key
if ([item.key isEqualToString:kFIRMessagingMessageSyncViaMCSKey]) {
BOOL value = [item.value boolValue];
message[kFIRMessagingMessageSyncViaMCSKey] = @(value);
continue;
}
// do not allow keys with "reserved" keyword
if ([[item.key lowercaseString] hasPrefix:kMessageReservedPrefix]) {
continue;
}
[message setObject:item.value forKey:item.key];
}
// TODO: Add support for encrypting raw data later
return [NSDictionary dictionaryWithDictionary:message];
}
- (void)didReceiveParsedMessage:(NSDictionary *)message {
if ([message[kFCMMessageSpecialMessage] length]) {
NSString *messageType = message[kFCMMessageSpecialMessage];
if ([kFCMMessageTypeDeletedMessages isEqualToString:messageType]) {
// TODO: Maybe trim down message to remove some unnecessary fields.
// tell the FCM receiver of deleted messages
[self.delegate didDeleteMessagesOnServer];
return;
}
FIRMessagingLoggerError(kFIRMessagingMessageCodeDataMessageManager002,
@"Invalid message type received: %@", messageType);
} else if (message[kFIRMessagingMessageSyncViaMCSKey]) {
// Update SYNC_RMQ with the message
BOOL isDuplicate = [self.syncMessageManager didReceiveMCSSyncMessage:message];
if (isDuplicate) {
return;
}
}
NSString *messageId = message[kFIRMessagingMessageIDKey];
NSDictionary *filteredMessage = [self filterInternalFIRMessagingKeysFromMessage:message];
[self.delegate didReceiveMessage:filteredMessage withIdentifier:messageId];
}
- (NSDictionary *)filterInternalFIRMessagingKeysFromMessage:(NSDictionary *)message {
NSMutableDictionary *newMessage = [NSMutableDictionary dictionaryWithDictionary:message];
for (NSString *key in message) {
if ([key hasPrefix:kFIRMessagingMessageInternalReservedKeyword]) {
[newMessage removeObjectForKey:key];
}
}
return [newMessage copy];
}
- (void)sendDataMessageStanza:(NSMutableDictionary *)dataMessage {
NSNumber *ttlNumber = dataMessage[kFIRMessagingSendTTL];
NSString *to = dataMessage[kFIRMessagingSendTo];
NSString *msgId = dataMessage[kFIRMessagingSendMessageID];
NSString *appPackage = [self categoryForUpstreamMessages];
GtalkDataMessageStanza *stanza = [[GtalkDataMessageStanza alloc] init];
// TODO: enforce TTL (right now only ttl=0 is special, means no storage)
int ttl = [ttlNumber intValue];
if (ttl < 0 || ttl > self.ttl) {
ttl = self.ttl;
}
[stanza setTtl:ttl];
[stanza setSent:FIRMessagingCurrentTimestampInSeconds()];
int delay = [self delayForMessage:dataMessage];
if (delay > 0) {
[stanza setMaxDelay:delay];
}
if (msgId) {
[stanza setId_p:msgId];
}
// collapse key as given by the sender
NSString *token = dataMessage[KFIRMessagingSendMessageAppData][kFIRMessagingCollapseKey];
if ([token length]) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager003,
@"FIRMessaging using %@ as collapse key", token);
[stanza setToken:token];
}
if (!self.secretToken) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager004,
@"Trying to send data message without a secret token. "
@"Authentication failed.");
[self willSendDataMessageFail:stanza
withMessageId:msgId
error:kFIRMessagingErrorCodeMissingDeviceID];
return;
}
if (![to length]) {
[self willSendDataMessageFail:stanza withMessageId:msgId error:kFIRMessagingErrorMissingTo];
return;
}
[stanza setTo:to];
[stanza setCategory:appPackage];
// required field in the proto this is set by the server
// set it to a sentinel so the runtime doesn't throw an exception
[stanza setFrom:@""];
// MCS itself would set the registration ID
// [stanza setRegId:nil];
int size = [self addData:dataMessage[KFIRMessagingSendMessageAppData] toStanza:stanza];
if (size > kMaxAppDataSizeDefault) {
[self willSendDataMessageFail:stanza withMessageId:msgId error:kFIRMessagingErrorSizeExceeded];
return;
}
BOOL useRmq = (ttl != 0) && (msgId != nil);
if (useRmq) {
if (!self.client.isConnected) {
// do nothing assuming rmq save is enabled
}
NSError *error;
if (![self.rmq2Manager saveRmqMessage:stanza error:&error]) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager005, @"%@", error);
[self willSendDataMessageFail:stanza withMessageId:msgId error:kFIRMessagingErrorSave];
return;
}
[self willSendDataMessageSuccess:stanza withMessageId:msgId];
}
// if delay > 0 we don't really care about sending the message right now
// so we piggy-back on any other urgent(delay = 0) message that we are sending
if (delay > 0 && [self delayMessage:stanza]) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager006, @"Delaying Message %@",
dataMessage);
return;
}
// send delayed messages
[self sendDelayedMessages:[self.delayedMessagesQueue removeDelayedMessages]];
BOOL sending = [self tryToSendDataMessageStanza:stanza];
if (!sending) {
if (useRmq) {
NSString *event __unused = [NSString stringWithFormat:@"Queued message: %@", [stanza id_p]];
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager007, @"%@", event);
} else {
[self willSendDataMessageFail:stanza
withMessageId:msgId
error:kFIRMessagingErrorCodeNetwork];
return;
}
}
}
- (void)sendDelayedMessages:(NSArray *)delayedMessages {
for (GtalkDataMessageStanza *message in delayedMessages) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager008,
@"%@ Sending delayed message %@", @"DMM", message);
[message setActualDelay:(int)(FIRMessagingCurrentTimestampInSeconds() - message.sent)];
[self tryToSendDataMessageStanza:message];
}
}
- (void)didSendDataMessageStanza:(GtalkDataMessageStanza *)message {
NSString *msgId = [message id_p] ?: @"";
[self.delegate didSendDataMessageWithID:msgId];
}
- (void)addParamWithKey:(NSString *)key
value:(NSString *)val
toStanza:(GtalkDataMessageStanza *)stanza {
if (!key || !val) {
return;
}
GtalkAppData *appData = [[GtalkAppData alloc] init];
[appData setKey:key];
[appData setValue:val];
[[stanza appDataArray] addObject:appData];
}
/**
@return The size of the data being added to stanza.
*/
- (int)addData:(NSDictionary *)data toStanza:(GtalkDataMessageStanza *)stanza {
int size = 0;
for (NSString *key in data) {
NSObject *val = data[key];
if ([val isKindOfClass:[NSString class]]) {
NSString *strVal = (NSString *)val;
[self addParamWithKey:key value:strVal toStanza:stanza];
size += [key length] + [strVal length];
} else if ([val isKindOfClass:[NSNumber class]]) {
NSString *strVal = [(NSNumber *)val stringValue];
[self addParamWithKey:key value:strVal toStanza:stanza];
size += [key length] + [strVal length];
} else if ([kFIRMessagingRawDataKey isEqualToString:key] &&
[val isKindOfClass:[NSData class]]) {
NSData *rawData = (NSData *)val;
[stanza setRawData:[rawData copy]];
size += [rawData length];
} else {
FIRMessagingLoggerError(kFIRMessagingMessageCodeDataMessageManager009, @"Ignoring key: %@",
key);
}
}
return size;
}
/**
* Notify the messenger that send data message completed with success. This is called for
* TTL=0, after the message has been sent, or when message is saved, to unlock the send()
* method.
*/
- (void)willSendDataMessageSuccess:(GtalkDataMessageStanza *)stanza
withMessageId:(NSString *)messageId {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager010,
@"send message success: %@", messageId);
[self.delegate willSendDataMessageWithID:messageId error:nil];
}
/**
* We send 'send failures' from server as normal FIRMessaging messages, with a 'message_type'
* extra - same as 'message deleted'.
*
* For TTL=0 or errors that can be detected during send ( too many messages, invalid, etc)
* we throw IOExceptions
*/
- (void)willSendDataMessageFail:(GtalkDataMessageStanza *)stanza
withMessageId:(NSString *)messageId
error:(FIRMessagingInternalErrorCode)errorCode {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager011,
@"Send message fail: %@ error: %lu", messageId, (unsigned long)errorCode);
NSError *error = [NSError errorWithFCMErrorCode:errorCode];
if ([self.delegate respondsToSelector:@selector(willSendDataMessageWithID:error:)]) {
[self.delegate willSendDataMessageWithID:messageId error:error];
}
}
- (void)resendMessagesWithConnection:(FIRMessagingConnection *)connection {
NSMutableString *rmqIdsResent = [NSMutableString string];
NSMutableArray *toRemoveRmqIds = [NSMutableArray array];
FIRMessaging_WEAKIFY(self);
FIRMessaging_WEAKIFY(connection);
FIRMessagingRmqMessageHandler messageHandler = ^(int64_t rmqId, int8_t tag, NSData *data) {
FIRMessaging_STRONGIFY(self);
FIRMessaging_STRONGIFY(connection);
GPBMessage *proto =
[FIRMessagingGetClassForTag((FIRMessagingProtoTag)tag) parseFromData:data error:NULL];
if ([proto isKindOfClass:GtalkDataMessageStanza.class]) {
GtalkDataMessageStanza *stanza = (GtalkDataMessageStanza *)proto;
if (![self handleExpirationForDataMessage:stanza]) {
// time expired let's delete from RMQ
[toRemoveRmqIds addObject:stanza.persistentId];
return;
}
[rmqIdsResent appendString:[NSString stringWithFormat:@"%@,", stanza.id_p]];
}
[connection sendProto:proto];
};
[self.rmq2Manager scanWithRmqMessageHandler:messageHandler
dataMessageHandler:nil];
if ([rmqIdsResent length]) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager012, @"Resent: %@",
rmqIdsResent);
}
if ([toRemoveRmqIds count]) {
[self.rmq2Manager removeRmqMessagesWithRmqIds:toRemoveRmqIds];
}
}
/**
* Check the TTL and generate an error if needed.
*
* @return false if the message needs to be deleted
*/
- (BOOL)handleExpirationForDataMessage:(GtalkDataMessageStanza *)message {
if (message.ttl == 0) {
return NO;
}
int64_t now = FIRMessagingCurrentTimestampInSeconds();
if (now > message.sent + message.ttl) {
[self willSendDataMessageFail:message
withMessageId:message.id_p
error:kFIRMessagingErrorServiceNotAvailable];
return NO;
}
return YES;
}
#pragma mark - Private
- (int)delayForMessage:(NSMutableDictionary *)message {
int delay = 0; // default
if (message[kFIRMessagingSendDelay]) {
delay = [message[kFIRMessagingSendDelay] intValue];
[message removeObjectForKey:kFIRMessagingSendDelay];
if (delay < kMinDelaySeconds) {
delay = 0;
} else if (delay > kMaxDelaySeconds) {
delay = kMaxDelaySeconds;
}
}
return delay;
}
// return True if successfully delayed else False
- (BOOL)delayMessage:(GtalkDataMessageStanza *)message {
return [self.delayedMessagesQueue queueMessage:message];
}
- (BOOL)tryToSendDataMessageStanza:(GtalkDataMessageStanza *)stanza {
if (self.client.isConnectionActive) {
[self.client sendMessage:stanza];
return YES;
}
// if we only reconnect for TTL = 0 messages check if we ttl = 0 or
// if we reconnect for all messages try to reconnect
if ((self.upstreamForceReconnect == kUpstreamForceReconnectTTL0 && stanza.ttl == 0) ||
self.upstreamForceReconnect == kUpstreamForceReconnectAll) {
BOOL isNetworkAvailable = [[FIRMessaging messaging] isNetworkAvailable];
if (isNetworkAvailable) {
if (stanza.ttl == 0) {
// Add TTL = 0 messages to be sent on next connect. TTL != 0 messages are
// persisted, and will be sent from the RMQ.
[self.client sendOnConnectOrDrop:stanza];
}
[self.client retryConnectionImmediately:YES];
return YES;
}
}
return NO;
}
- (NSString *)categoryForUpstreamMessages {
return FIRMessagingAppIdentifier();
}
@end
@@ -0,0 +1,84 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef FIRMessaging_xcodeproj_FIRMessagingDefines_h
#define FIRMessaging_xcodeproj_FIRMessagingDefines_h
#define _FIRMessaging_VERBOSE_LOGGING 1
// Verbose Logging
#if (_FIRMessaging_VERBOSE_LOGGING)
#define FIRMessaging_DEV_VERBOSE_LOG(...) NSLog(__VA_ARGS__)
#else
#define FIRMessaging_DEV_VERBOSE_LOG(...) do { } while (0)
#endif // FIRMessaging_VERBOSE_LOGGING
// WEAKIFY & STRONGIFY
// Helper macro.
#define _FIRMessaging_WEAKNAME(VAR) VAR ## _weak_
#define FIRMessaging_WEAKIFY(VAR) __weak __typeof__(VAR) _FIRMessaging_WEAKNAME(VAR) = (VAR);
#define FIRMessaging_STRONGIFY(VAR) \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Wshadow\"") \
__strong __typeof__(VAR) VAR = _FIRMessaging_WEAKNAME(VAR); \
_Pragma("clang diagnostic pop")
// Type Conversions (used for NSInteger etc)
#ifndef _FIRMessaging_L
#define _FIRMessaging_L(v) (long)(v)
#endif
#ifndef _FIRMessaging_UL
#define _FIRMessaging_UL(v) (unsigned long)(v)
#endif
#endif
// Debug Assert
#ifndef _FIRMessagingDevAssert
// we directly invoke the NSAssert handler so we can pass on the varargs
// (NSAssert doesn't have a macro we can use that takes varargs)
#ifdef DEBUG
#define _FIRMessagingDevAssert(condition, ...) \
do { \
if (!(condition)) { \
[[NSAssertionHandler currentHandler] \
handleFailureInFunction:(NSString *) \
[NSString stringWithUTF8String:__PRETTY_FUNCTION__] \
file:(NSString *)[NSString stringWithUTF8String:__FILE__] \
lineNumber:__LINE__ \
description:__VA_ARGS__]; \
} \
} while(0)
#else // !defined(DEBUG)
#define _FIRMessagingDevAssert(condition, ...) do { } while (0)
#endif // !defined(DEBUG)
#endif // _FIRMessagingDevAssert
// Invalidates the initializer from which it's called.
#ifndef FIRMessagingInvalidateInitializer
#define FIRMessagingInvalidateInitializer() \
do { \
[self class]; /* Avoid warning of dead store to |self|. */ \
_FIRMessagingDevAssert(NO, @"Invalid initializer."); \
return nil; \
} while (0)
#endif
@@ -0,0 +1,35 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
@class GtalkDataMessageStanza;
@class FIRMessagingRmqManager;
@protocol FIRMessagingRmqScanner;
typedef void(^FIRMessagingSendDelayedMessagesHandler)(NSArray *messages);
@interface FIRMessagingDelayedMessageQueue : NSObject
- (instancetype)initWithRmqScanner:(id<FIRMessagingRmqScanner>)rmqScanner
sendDelayedMessagesHandler:(FIRMessagingSendDelayedMessagesHandler)sendDelayedMessagesHandler;
- (BOOL)queueMessage:(GtalkDataMessageStanza *)message;
- (NSArray *)removeDelayedMessages;
@end
@@ -0,0 +1,146 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessagingDelayedMessageQueue.h"
#import "Protos/GtalkCore.pbobjc.h"
#import "FIRMessagingDefines.h"
#import "FIRMessagingRmqManager.h"
#import "FIRMessagingUtilities.h"
static const int kMaxQueuedMessageCount = 10;
@interface FIRMessagingDelayedMessageQueue ()
@property(nonatomic, readonly, weak) id<FIRMessagingRmqScanner> rmqScanner;
@property(nonatomic, readonly, copy) FIRMessagingSendDelayedMessagesHandler sendDelayedMessagesHandler;
@property(nonatomic, readwrite, assign) int persistedMessageCount;
// the scheduled timeout or -1 if not set
@property(nonatomic, readwrite, assign) int64_t scheduledTimeoutMilliseconds;
// The time of the last scan of the message DB,
// used to avoid retrieving messages more than once.
@property(nonatomic, readwrite, assign) int64_t lastDBScanTimestampSeconds;
@property(nonatomic, readwrite, strong) NSMutableArray *messages;
@property(nonatomic, readwrite, strong) NSTimer *sendTimer;
@end
@implementation FIRMessagingDelayedMessageQueue
- (instancetype)init {
FIRMessagingInvalidateInitializer();
}
- (instancetype)initWithRmqScanner:(id<FIRMessagingRmqScanner>)rmqScanner
sendDelayedMessagesHandler:(FIRMessagingSendDelayedMessagesHandler)sendDelayedMessagesHandler {
_FIRMessagingDevAssert(sendDelayedMessagesHandler, @"Invalid nil callback for delayed messages");
self = [super init];
if (self) {
_rmqScanner = rmqScanner;
_sendDelayedMessagesHandler = sendDelayedMessagesHandler;
_messages = [NSMutableArray arrayWithCapacity:10];
_scheduledTimeoutMilliseconds = -1;
}
return self;
}
- (BOOL)queueMessage:(GtalkDataMessageStanza *)message {
if (self.messages.count >= kMaxQueuedMessageCount) {
return NO;
}
if (message.ttl == 0) {
// ttl=0 messages aren't persisted, add it to memory
[self.messages addObject:message];
} else {
self.persistedMessageCount++;
}
int64_t timeoutMillis = [self calculateTimeoutInMillisWithDelayInSeconds:message.maxDelay];
if (![self isTimeoutScheduled] || timeoutMillis < self.scheduledTimeoutMilliseconds) {
[self scheduleTimeoutInMillis:timeoutMillis];
}
return YES;
}
- (NSArray *)removeDelayedMessages {
[self cancelTimeout];
if ([self messageCount] == 0) {
return @[];
}
NSMutableArray *delayedMessages = [NSMutableArray array];
// add the ttl=0 messages
if (self.messages.count) {
[delayedMessages addObjectsFromArray:delayedMessages];
[self.messages removeAllObjects];
}
// add persistent messages
if (self.persistedMessageCount > 0) {
FIRMessaging_WEAKIFY(self);
[self.rmqScanner scanWithRmqMessageHandler:nil
dataMessageHandler:^(int64_t rmqId, GtalkDataMessageStanza *stanza) {
FIRMessaging_STRONGIFY(self);
if ([stanza hasMaxDelay] &&
[stanza sent] >= self.lastDBScanTimestampSeconds) {
[delayedMessages addObject:stanza];
}
}];
self.lastDBScanTimestampSeconds = FIRMessagingCurrentTimestampInSeconds();
self.persistedMessageCount = 0;
}
return delayedMessages;
}
- (void)sendMessages {
if (self.sendDelayedMessagesHandler) {
self.sendDelayedMessagesHandler([self removeDelayedMessages]);
}
}
#pragma mark - Private
- (NSInteger)messageCount {
return self.messages.count + self.persistedMessageCount;
}
- (BOOL)isTimeoutScheduled {
return self.scheduledTimeoutMilliseconds > 0;
}
- (int64_t)calculateTimeoutInMillisWithDelayInSeconds:(int)delay {
return FIRMessagingCurrentTimestampInMilliseconds() + delay * 1000.0;
}
- (void)scheduleTimeoutInMillis:(int64_t)time {
[self cancelTimeout];
self.scheduledTimeoutMilliseconds = time;
double delay = (time - FIRMessagingCurrentTimestampInMilliseconds()) / 1000.0;
[self performSelector:@selector(sendMessages) withObject:self afterDelay:delay];
}
- (void)cancelTimeout {
if ([self isTimeoutScheduled]) {
[NSObject cancelPreviousPerformRequestsWithTarget:self
selector:@selector(sendMessages)
object:nil];
self.scheduledTimeoutMilliseconds = -1;
}
}
@end
@@ -0,0 +1,118 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessagingExtensionHelper.h"
#import "FIRMMessageCode.h"
#import "FIRMessagingLogger.h"
static NSString *const kPayloadOptionsName = @"fcm_options";
static NSString *const kPayloadOptionsImageURLName = @"image";
@interface FIRMessagingExtensionHelper ()
@property(nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
@property(nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
@end
@implementation FIRMessagingExtensionHelper
- (void)populateNotificationContent:(UNMutableNotificationContent *)content
withContentHandler:(void (^)(UNNotificationContent *_Nonnull))contentHandler {
self.contentHandler = [contentHandler copy];
self.bestAttemptContent = content;
// The `userInfo` property isn't available on newer versions of tvOS.
#if TARGET_OS_IOS || TARGET_OS_OSX
NSString *currentImageURL = content.userInfo[kPayloadOptionsName][kPayloadOptionsImageURLName];
if (!currentImageURL) {
[self deliverNotification];
return;
}
NSURL *attachmentURL = [NSURL URLWithString:currentImageURL];
if (attachmentURL) {
[self loadAttachmentForURL:attachmentURL
completionHandler:^(UNNotificationAttachment *attachment) {
self.bestAttemptContent.attachments = @[ attachment ];
[self deliverNotification];
}];
} else {
FIRMessagingLoggerError(kFIRMessagingServiceExtensionImageInvalidURL,
@"The Image URL provided is invalid %@.", currentImageURL);
[self deliverNotification];
}
#else
[self deliverNotification];
#endif
}
#if TARGET_OS_IOS || TARGET_OS_OSX
- (void)loadAttachmentForURL:(NSURL *)attachmentURL
completionHandler:(void (^)(UNNotificationAttachment *))completionHandler {
__block UNNotificationAttachment *attachment = nil;
NSURLSession *session = [NSURLSession
sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
[[session
downloadTaskWithURL:attachmentURL
completionHandler:^(NSURL *temporaryFileLocation, NSURLResponse *response, NSError *error) {
if (error != nil) {
FIRMessagingLoggerError(kFIRMessagingServiceExtensionImageNotDownloaded,
@"Failed to download image given URL %@, error: %@\n",
attachmentURL, error);
completionHandler(attachment);
return;
}
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *fileExtension =
[NSString stringWithFormat:@".%@", [response.suggestedFilename pathExtension]];
NSURL *localURL = [NSURL
fileURLWithPath:[temporaryFileLocation.path stringByAppendingString:fileExtension]];
[fileManager moveItemAtURL:temporaryFileLocation toURL:localURL error:&error];
if (error) {
FIRMessagingLoggerError(
kFIRMessagingServiceExtensionLocalFileNotCreated,
@"Failed to move the image file to local location: %@, error: %@\n", localURL,
error);
completionHandler(attachment);
return;
}
attachment = [UNNotificationAttachment attachmentWithIdentifier:@""
URL:localURL
options:nil
error:&error];
if (error) {
FIRMessagingLoggerError(kFIRMessagingServiceExtensionImageNotAttached,
@"Failed to create attachment with URL %@, error: %@\n",
localURL, error);
completionHandler(attachment);
return;
}
completionHandler(attachment);
}] resume];
}
#endif
- (void)deliverNotification {
if (self.contentHandler) {
self.contentHandler(self.bestAttemptContent);
}
}
@end
@@ -0,0 +1,67 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMMessageCode.h"
// The convenience macros are only defined if they haven't already been defined.
#ifndef FIRMessagingLoggerInfo
// Convenience macros that log to the shared FIRMessagingLogger instance. These macros
// are how users should typically log to FIRMessagingLogger.
#define FIRMessagingLoggerDebug(code, ...) \
[FIRMessagingSharedLogger() logFuncDebug:__func__ messageCode:code msg:__VA_ARGS__]
#define FIRMessagingLoggerInfo(code, ...) \
[FIRMessagingSharedLogger() logFuncInfo:__func__ messageCode:code msg:__VA_ARGS__]
#define FIRMessagingLoggerNotice(code, ...) \
[FIRMessagingSharedLogger() logFuncNotice:__func__ messageCode:code msg:__VA_ARGS__]
#define FIRMessagingLoggerWarn(code, ...) \
[FIRMessagingSharedLogger() logFuncWarning:__func__ messageCode:code msg:__VA_ARGS__]
#define FIRMessagingLoggerError(code, ...) \
[FIRMessagingSharedLogger() logFuncError:__func__ messageCode:code msg:__VA_ARGS__]
#endif // !defined(FIRMessagingLoggerInfo)
@interface FIRMessagingLogger : NSObject
- (void)logFuncDebug:(const char *)func
messageCode:(FIRMessagingMessageCode)messageCode
msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4);
- (void)logFuncInfo:(const char *)func
messageCode:(FIRMessagingMessageCode)messageCode
msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4);
- (void)logFuncNotice:(const char *)func
messageCode:(FIRMessagingMessageCode)messageCode
msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4);
- (void)logFuncWarning:(const char *)func
messageCode:(FIRMessagingMessageCode)messageCode
msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4);
- (void)logFuncError:(const char *)func
messageCode:(FIRMessagingMessageCode)messageCode
msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4);
@end
/**
* Instantiates and/or returns a shared FIRMessagingLogger used exclusively
* for FIRMessaging log messages.
*
* @return the shared FIRMessagingLogger instance
*/
FIRMessagingLogger *FIRMessagingSharedLogger(void);
@@ -0,0 +1,95 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessagingLogger.h"
#import <FirebaseCore/FIRLogger.h>
FIRLoggerService kFIRLoggerMessaging = @"[Firebase/Messaging]";
@implementation FIRMessagingLogger
+ (instancetype)standardLogger {
return [[FIRMessagingLogger alloc] init];
}
#pragma mark - Log Helpers
+ (NSString *)formatMessageCode:(FIRMessagingMessageCode)messageCode {
return [NSString stringWithFormat:@"I-FCM%06ld", (long)messageCode];
}
- (void)logFuncDebug:(const char *)func
messageCode:(FIRMessagingMessageCode)messageCode
msg:(NSString *)fmt, ... {
va_list args;
va_start(args, fmt);
FIRLogBasic(FIRLoggerLevelDebug, kFIRLoggerMessaging,
[FIRMessagingLogger formatMessageCode:messageCode], fmt, args);
va_end(args);
}
- (void)logFuncInfo:(const char *)func
messageCode:(FIRMessagingMessageCode)messageCode
msg:(NSString *)fmt, ... {
va_list args;
va_start(args, fmt);
FIRLogBasic(FIRLoggerLevelInfo, kFIRLoggerMessaging,
[FIRMessagingLogger formatMessageCode:messageCode], fmt, args);
va_end(args);
}
- (void)logFuncNotice:(const char *)func
messageCode:(FIRMessagingMessageCode)messageCode
msg:(NSString *)fmt, ... {
va_list args;
va_start(args, fmt);
FIRLogBasic(FIRLoggerLevelNotice, kFIRLoggerMessaging,
[FIRMessagingLogger formatMessageCode:messageCode], fmt, args);
va_end(args);
}
- (void)logFuncWarning:(const char *)func
messageCode:(FIRMessagingMessageCode)messageCode
msg:(NSString *)fmt, ... {
va_list args;
va_start(args, fmt);
FIRLogBasic(FIRLoggerLevelWarning, kFIRLoggerMessaging,
[FIRMessagingLogger formatMessageCode:messageCode], fmt, args);
va_end(args);
}
- (void)logFuncError:(const char *)func
messageCode:(FIRMessagingMessageCode)messageCode
msg:(NSString *)fmt, ... {
va_list args;
va_start(args, fmt);
FIRLogBasic(FIRLoggerLevelError, kFIRLoggerMessaging,
[FIRMessagingLogger formatMessageCode:messageCode], fmt, args);
va_end(args);
}
@end
FIRMessagingLogger *FIRMessagingSharedLogger(void) {
static dispatch_once_t onceToken;
static FIRMessagingLogger *logger;
dispatch_once(&onceToken, ^{
logger = [FIRMessagingLogger standardLogger];
});
return logger;
}
@@ -0,0 +1,43 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
@interface FIRMessagingPacket : NSObject
+ (FIRMessagingPacket *)packetWithTag:(int8_t)tag rmqId:(NSString *)rmqId data:(NSData *)data;
@property(nonatomic, readonly, strong) NSData *data;
@property(nonatomic, readonly, assign) int8_t tag;
// not sent over the wire required for bookkeeping
@property(nonatomic, readonly, assign) NSString *rmqId;
@end
/**
* A queue of the packets(protos) that need to be send over the wire.
*/
@interface FIRMessagingPacketQueue : NSObject
@property(nonatomic, readonly, assign) NSUInteger count;
@property(nonatomic, readonly, assign) BOOL isEmpty;
- (void)push:(FIRMessagingPacket *)packet;
- (void)pushHead:(FIRMessagingPacket *)packet;
- (FIRMessagingPacket *)pop;
@end
@@ -0,0 +1,103 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessagingPacketQueue.h"
#import "FIRMessagingDefines.h"
@interface FIRMessagingPacket ()
@property(nonatomic, readwrite, strong) NSData *data;
@property(nonatomic, readwrite, assign) int8_t tag;
@property(nonatomic, readwrite, assign) NSString *rmqId;
@end
@implementation FIRMessagingPacket
+ (FIRMessagingPacket *)packetWithTag:(int8_t)tag rmqId:(NSString *)rmqId data:(NSData *)data {
return [[self alloc] initWithTag:tag rmqId:rmqId data:data];
}
- (instancetype)init {
FIRMessagingInvalidateInitializer();
}
- (instancetype)initWithTag:(int8_t)tag rmqId:(NSString *)rmqId data:(NSData *)data {
self = [super init];
if (self != nil) {
_data = data;
_tag = tag;
_rmqId = rmqId;
}
return self;
}
- (NSString *)description {
if ([self.rmqId length]) {
return [NSString stringWithFormat:@"<Packet: Tag - %d, Length - %lu>, RmqId - %@",
self.tag, _FIRMessaging_UL(self.data.length), self.rmqId];
} else {
return [NSString stringWithFormat:@"<Packet: Tag - %d, Length - %lu>",
self.tag, _FIRMessaging_UL(self.data.length)];
}
}
@end
@interface FIRMessagingPacketQueue ()
@property(nonatomic, readwrite, strong) NSMutableArray *packetsContainer;
@end
@implementation FIRMessagingPacketQueue;
- (id)init {
self = [super init];
if (self) {
_packetsContainer = [[NSMutableArray alloc] init];
}
return self;
}
- (BOOL)isEmpty {
return self.packetsContainer.count == 0;
}
- (NSUInteger)count {
return self.packetsContainer.count;
}
- (void)push:(FIRMessagingPacket *)packet {
[self.packetsContainer addObject:packet];
}
- (void)pushHead:(FIRMessagingPacket *)packet {
[self.packetsContainer insertObject:packet atIndex:0];
}
- (FIRMessagingPacket *)pop {
if (!self.isEmpty) {
FIRMessagingPacket *packet = self.packetsContainer[0];
[self.packetsContainer removeObjectAtIndex:0];
return packet;
}
return nil;
}
@end
@@ -0,0 +1,119 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
#import "FIRMessaging.h"
#import "FIRMessagingTopicsCommon.h"
NS_ASSUME_NONNULL_BEGIN
/**
* Represents a single batch of topics, with the same action.
*
* Topic operations which have the same action (subscribe or unsubscribe) can be executed
* simultaneously, as the order of operations do not matter with the same action. The set of
* topics is unique, as it doesn't make sense to apply the same action to the same topic
* repeatedly; the result would be the same as the first time.
*/
@interface FIRMessagingTopicBatch : NSObject <NSCoding>
@property(nonatomic, readonly, assign) FIRMessagingTopicAction action;
@property(nonatomic, readonly, copy) NSMutableSet <NSString *> *topics;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithAction:(FIRMessagingTopicAction)action NS_DESIGNATED_INITIALIZER;
@end
@class FIRMessagingPendingTopicsList;
/**
* This delegate must be supplied to the instance of FIRMessagingPendingTopicsList, via the
* @cdelegate property. It lets the
* pending topics list know whether or not it can begin making requests via
* @c-pendingTopicsListCanRequestTopicUpdates:, and handles the request to actually
* perform the topic operation. The delegate also handles when the pending topics list is updated,
* so that it can be archived or persisted.
*
* @see FIRMessagingPendingTopicsList
*/
@protocol FIRMessagingPendingTopicsListDelegate <NSObject>
- (void)pendingTopicsList:(FIRMessagingPendingTopicsList *)list
requestedUpdateForTopic:(NSString *)topic
action:(FIRMessagingTopicAction)action
completion:(FIRMessagingTopicOperationCompletion)completion;
- (void)pendingTopicsListDidUpdate:(FIRMessagingPendingTopicsList *)list;
- (BOOL)pendingTopicsListCanRequestTopicUpdates:(FIRMessagingPendingTopicsList *)list;
@end
/**
* FIRMessagingPendingTopicsList manages a list of topic subscription updates, batched by the same
* action (subscribe or unsubscribe). The list roughly maintains the order of the topic operations,
* batched together whenever the topic action (subscribe or unsubscribe) changes.
*
* Topics operations are batched by action because it is safe to perform the same topic action
* (subscribe or unsubscribe) on many topics simultaneously. After each batch is successfully
* completed, the next batch operations can begin.
*
* When asked to resume its operations, FIRMessagingPendingTopicsList will begin performing updates
* of its current batch of topics. For example, it may begin subscription operations for topics
* [A, B, C] simultaneously.
*
* When the current batch is completed, the next batch of operations will be started. For example
* the list may begin unsubscribe operations for [D, A, E]. Note that because A is in both batches,
* A will be correctly subscribed in the first batch, then unsubscribed as part of the second batch
* of operations. Without batching, it would be ambiguous whether A's subscription operation or the
* unsubscription operation would be completed first.
*
* An app can subscribe and unsubscribe from many topics, and this class helps persist the pending
* topics and perform the operation safely and correctly.
*
* When a topic fails to subscribe or unsubscribe due to a network error, it is considered a
* recoverable error, and so it remains in the current batch until it is succesfully completed.
* Topic updates are completed when they either (a) succeed, (b) are cancelled, or (c) result in an
* unrecoverable error. Any error outside of `NSURLErrorDomain` is considered an unrecoverable
* error.
*
* In addition to maintaining the list of pending topic updates, FIRMessagingPendingTopicsList also
* can track completion handlers for topic operations.
*
* @discussion Completion handlers for topic updates are not maintained if it was restored from a
* keyed archive. They are only called if the topic operation finished within the same app session.
*
* You must supply an object conforming to FIRMessagingPendingTopicsListDelegate in order for the
* topic operations to execute.
*
* @see FIRMessagingPendingTopicsListDelegate
*/
@interface FIRMessagingPendingTopicsList : NSObject <NSCoding>
@property(nonatomic, weak) NSObject <FIRMessagingPendingTopicsListDelegate> *delegate;
@property(nonatomic, readonly, strong, nullable) NSDate *archiveDate;
@property(nonatomic, readonly) NSUInteger numberOfBatches;
- (instancetype)init NS_DESIGNATED_INITIALIZER;
- (void)addOperationForTopic:(NSString *)topic
withAction:(FIRMessagingTopicAction)action
completion:(nullable FIRMessagingTopicOperationCompletion)completion;
- (void)resumeOperationsIfNeeded;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,265 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessagingPendingTopicsList.h"
#import "FIRMessaging_Private.h"
#import "FIRMessagingLogger.h"
#import "FIRMessagingPubSub.h"
#import "FIRMessagingDefines.h"
NSString *const kPendingTopicBatchActionKey = @"action";
NSString *const kPendingTopicBatchTopicsKey = @"topics";
NSString *const kPendingBatchesEncodingKey = @"batches";
NSString *const kPendingTopicsTimestampEncodingKey = @"ts";
#pragma mark - FIRMessagingTopicBatch
@interface FIRMessagingTopicBatch ()
@property(nonatomic, strong, nonnull) NSMutableDictionary
<NSString *, NSMutableArray <FIRMessagingTopicOperationCompletion> *> *topicHandlers;
@end
@implementation FIRMessagingTopicBatch
- (instancetype)initWithAction:(FIRMessagingTopicAction)action {
if (self = [super init]) {
_action = action;
_topics = [NSMutableSet set];
_topicHandlers = [NSMutableDictionary dictionary];
}
return self;
}
#pragma mark NSCoding
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeInteger:self.action forKey:kPendingTopicBatchActionKey];
[aCoder encodeObject:self.topics forKey:kPendingTopicBatchTopicsKey];
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
// Ensure that our integer -> enum casting is safe
NSInteger actionRawValue = [aDecoder decodeIntegerForKey:kPendingTopicBatchActionKey];
FIRMessagingTopicAction action = FIRMessagingTopicActionSubscribe;
if (actionRawValue == FIRMessagingTopicActionUnsubscribe) {
action = FIRMessagingTopicActionUnsubscribe;
}
if (self = [self initWithAction:action]) {
NSSet *topics = [aDecoder decodeObjectForKey:kPendingTopicBatchTopicsKey];
if ([topics isKindOfClass:[NSSet class]]) {
_topics = [topics mutableCopy];
}
_topicHandlers = [NSMutableDictionary dictionary];
}
return self;
}
@end
#pragma mark - FIRMessagingPendingTopicsList
@interface FIRMessagingPendingTopicsList ()
@property(nonatomic, readwrite, strong) NSDate *archiveDate;
@property(nonatomic, strong) NSMutableArray <FIRMessagingTopicBatch *> *topicBatches;
@property(nonatomic, strong) FIRMessagingTopicBatch *currentBatch;
@property(nonatomic, strong) NSMutableSet <NSString *> *topicsInFlight;
@end
@implementation FIRMessagingPendingTopicsList
- (instancetype)init {
if (self = [super init]) {
_topicBatches = [NSMutableArray array];
_topicsInFlight = [NSMutableSet set];
}
return self;
}
+ (void)pruneTopicBatches:(NSMutableArray <FIRMessagingTopicBatch *> *)topicBatches {
// For now, just remove empty batches. In the future we can use this to make the subscriptions
// more efficient, by actually pruning topic actions that cancel each other out, for example.
for (NSInteger i = topicBatches.count-1; i >= 0; i--) {
FIRMessagingTopicBatch *batch = topicBatches[i];
if (batch.topics.count == 0) {
[topicBatches removeObjectAtIndex:i];
}
}
}
#pragma mark NSCoding
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:[NSDate date] forKey:kPendingTopicsTimestampEncodingKey];
[aCoder encodeObject:self.topicBatches forKey:kPendingBatchesEncodingKey];
}
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [self init]) {
_archiveDate = [aDecoder decodeObjectForKey:kPendingTopicsTimestampEncodingKey];
NSArray *archivedBatches = [aDecoder decodeObjectForKey:kPendingBatchesEncodingKey];
if (archivedBatches) {
_topicBatches = [archivedBatches mutableCopy];
[FIRMessagingPendingTopicsList pruneTopicBatches:_topicBatches];
}
_topicsInFlight = [NSMutableSet set];
}
return self;
}
#pragma mark Getters
- (NSUInteger)numberOfBatches {
return self.topicBatches.count;
}
#pragma mark Adding/Removing topics
- (void)addOperationForTopic:(NSString *)topic
withAction:(FIRMessagingTopicAction)action
completion:(nullable FIRMessagingTopicOperationCompletion)completion {
FIRMessagingTopicBatch *lastBatch = nil;
@synchronized (self) {
lastBatch = self.topicBatches.lastObject;
if (!lastBatch || lastBatch.action != action) {
// There either was no last batch, or our last batch's action was not the same, so we have to
// create a new batch
lastBatch = [[FIRMessagingTopicBatch alloc] initWithAction:action];
[self.topicBatches addObject:lastBatch];
}
BOOL topicExistedBefore = ([lastBatch.topics member:topic] != nil);
if (!topicExistedBefore) {
[lastBatch.topics addObject:topic];
[self.delegate pendingTopicsListDidUpdate:self];
}
// Add the completion handler to the batch
if (completion) {
NSMutableArray *handlers = lastBatch.topicHandlers[topic];
if (!handlers) {
handlers = [[NSMutableArray alloc] init];
}
[handlers addObject:completion];
lastBatch.topicHandlers[topic] = handlers;
}
if (!self.currentBatch) {
self.currentBatch = lastBatch;
}
// This may have been the first topic added, or was added to an ongoing batch
if (self.currentBatch == lastBatch && !topicExistedBefore) {
// Add this topic to our ongoing operations
FIRMessaging_WEAKIFY(self);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
FIRMessaging_STRONGIFY(self);
[self resumeOperationsIfNeeded];
});
}
}
}
- (void)resumeOperationsIfNeeded {
@synchronized (self) {
// If current batch is not set, set it now
if (!self.currentBatch) {
self.currentBatch = self.topicBatches.firstObject;
}
if (self.currentBatch.topics.count == 0) {
return;
}
if (!self.delegate) {
FIRMessagingLoggerError(kFIRMessagingMessageCodePendingTopicsList000,
@"Attempted to update pending topics without a delegate");
return;
}
if (![self.delegate pendingTopicsListCanRequestTopicUpdates:self]) {
return;
}
for (NSString *topic in self.currentBatch.topics) {
if ([self.topicsInFlight member:topic]) {
// This topic is already active, so skip
continue;
}
[self beginUpdateForCurrentBatchTopic:topic];
}
}
}
- (BOOL)subscriptionErrorIsRecoverable:(NSError *)error {
return [error.domain isEqualToString:NSURLErrorDomain];
}
- (void)beginUpdateForCurrentBatchTopic:(NSString *)topic {
@synchronized (self) {
[self.topicsInFlight addObject:topic];
}
FIRMessaging_WEAKIFY(self);
[self.delegate
pendingTopicsList:self
requestedUpdateForTopic:topic
action:self.currentBatch.action
completion:^(NSError *error) {
dispatch_async(
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
FIRMessaging_STRONGIFY(self);
@synchronized(self) {
[self.topicsInFlight removeObject:topic];
BOOL recoverableError = [self subscriptionErrorIsRecoverable:error];
if (!error || !recoverableError) {
// Notify our handlers and remove the topic from our batch
NSMutableArray *handlers = self.currentBatch.topicHandlers[topic];
if (handlers.count) {
dispatch_async(dispatch_get_main_queue(), ^{
for (FIRMessagingTopicOperationCompletion handler in handlers) {
handler(error);
}
[handlers removeAllObjects];
});
}
[self.currentBatch.topics removeObject:topic];
[self.currentBatch.topicHandlers removeObjectForKey:topic];
if (self.currentBatch.topics.count == 0) {
// All topic updates successfully finished in this batch, move on
// to the next batch
[self.topicBatches removeObject:self.currentBatch];
self.currentBatch = nil;
}
[self.delegate pendingTopicsListDidUpdate:self];
FIRMessaging_WEAKIFY(self);
dispatch_async(
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0),
^{
FIRMessaging_STRONGIFY(self);
[self resumeOperationsIfNeeded];
});
}
}
});
}];
}
@end
@@ -0,0 +1,28 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
@interface FIRMessagingPersistentSyncMessage : NSObject
@property(nonatomic, readonly, strong) NSString *rmqID;
@property(nonatomic, readwrite, assign) BOOL apnsReceived;
@property(nonatomic, readwrite, assign) BOOL mcsReceived;
@property(nonatomic, readonly, assign) int64_t expirationTime;
- (instancetype)initWithRMQID:(NSString *)rmqID expirationTime:(int64_t)expirationTime;
@end
@@ -0,0 +1,54 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessagingPersistentSyncMessage.h"
#import "FIRMessagingDefines.h"
@interface FIRMessagingPersistentSyncMessage ()
@property(nonatomic, readwrite, strong) NSString *rmqID;
@property(nonatomic, readwrite, assign) int64_t expirationTime;
@end
@implementation FIRMessagingPersistentSyncMessage
- (instancetype)init {
FIRMessagingInvalidateInitializer();
}
- (instancetype)initWithRMQID:(NSString *)rmqID expirationTime:(int64_t)expirationTime {
self = [super init];
if (self) {
_rmqID = [rmqID copy];
_expirationTime = expirationTime;
}
return self;
}
- (NSString *)description {
NSString *classDescription = NSStringFromClass([self class]);
NSDate *date = [NSDate dateWithTimeIntervalSince1970:self.expirationTime];
return [NSString stringWithFormat:@"%@: (rmqID: %@, apns: %d, mcs: %d, expiry: %@",
classDescription, self.rmqID, self.mcsReceived, self.apnsReceived, date];
}
- (NSString *)debugDescription {
return [self description];
}
@end
@@ -0,0 +1,171 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessaging.h"
NS_ASSUME_NONNULL_BEGIN
@class FIRMessagingClient;
@class FIRMessagingPubSubCache;
/**
* FIRMessagingPubSub provides a publish-subscribe model for sending FIRMessaging topic messages.
*
* An app can subscribe to different topics defined by the
* developer. The app server can then send messages to the subscribed devices
* without having to maintain topic-subscribers mapping. Topics do not
* need to be explicitly created before subscribing or publishing&mdash;they
* are automatically created when publishing or subscribing.
*
* Messages published to the topic will be received as regular FIRMessaging messages
* with `"from"` set to `"/topics/myTopic"`.
*
* Only topic names that match the pattern `"/topics/[a-zA-Z0-9-_.~%]{1,900}"`
* are allowed for subscribing and publishing.
*/
@interface FIRMessagingPubSub : NSObject
@property(nonatomic, readonly, strong) FIRMessagingPubSubCache *cache;
@property(nonatomic, readonly, strong) FIRMessagingClient *client;
/**
* Initializes an instance of FIRMessagingPubSub.
*
* @return An instance of FIRMessagingPubSub.
*/
- (instancetype)initWithClient:(FIRMessagingClient *)client NS_DESIGNATED_INITIALIZER;
/**
* Subscribes an app instance to a topic, enabling it to receive messages
* sent to that topic.
*
* This is an asynchronous call. If subscription fails, FIRMessaging
* invokes the completion callback with the appropriate error.
*
* @see FIRMessagingPubSub unsubscribeWithToken:topic:handler:
*
* @param token The registration token as received from the InstanceID
* library for a given `authorizedEntity` and "gcm" scope.
* @param topic The topic to subscribe to. Should be of the form
* `"/topics/<topic-name>"`.
* @param options Unused parameter, please pass nil or empty dictionary.
* @param handler The callback handler invoked when the subscribe call
* ends. In case of success, a nil error is returned. Otherwise,
* an appropriate error object is returned.
* @discussion This method is thread-safe. However, it is not guaranteed to
* return on the main thread.
*/
- (void)subscribeWithToken:(NSString *)token
topic:(NSString *)topic
options:(nullable NSDictionary *)options
handler:(FIRMessagingTopicOperationCompletion)handler;
/**
* Unsubscribes an app instance from a topic, stopping it from receiving
* any further messages sent to that topic.
*
* This is an asynchronous call. If the attempt to unsubscribe fails,
* we invoke the `completion` callback passed in with an appropriate error.
*
* @param token The token used to subscribe to this topic.
* @param topic The topic to unsubscribe from. Should be of the form
* `"/topics/<topic-name>"`.
* @param options Unused parameter, please pass nil or empty dictionary.
* @param handler The handler that is invoked once the unsubscribe call ends.
* In case of success, nil error is returned. Otherwise, an
* appropriate error object is returned.
* @discussion This method is thread-safe. However, it is not guaranteed to
* return on the main thread.
*/
- (void)unsubscribeWithToken:(NSString *)token
topic:(NSString *)topic
options:(nullable NSDictionary *)options
handler:(FIRMessagingTopicOperationCompletion)handler;
/**
* Asynchronously subscribe to the topic. Adds to the pending list of topic operations.
* Retry in case of failures. This makes a repeated attempt to subscribe to the topic
* as compared to the `subscribe` method above which tries once.
*
* @param topic The topic name to subscribe to. Should be of the form `"/topics/<topic-name>"`.
* @param handler The handler that is invoked once the unsubscribe call ends.
* In case of success, nil error is returned. Otherwise, an
* appropriate error object is returned.
*/
- (void)subscribeToTopic:(NSString *)topic
handler:(nullable FIRMessagingTopicOperationCompletion)handler;
/**
* Asynchronously unsubscribe from the topic. Adds to the pending list of topic operations.
* Retry in case of failures. This makes a repeated attempt to unsubscribe from the topic
* as compared to the `unsubscribe` method above which tries once.
*
* @param topic The topic name to unsubscribe from. Should be of the form `"/topics/<topic-name>"`.
* @param handler The handler that is invoked once the unsubscribe call ends.
* In case of success, nil error is returned. Otherwise, an
* appropriate error object is returned.
*/
- (void)unsubscribeFromTopic:(NSString *)topic
handler:(nullable FIRMessagingTopicOperationCompletion)handler;
/**
* Schedule subscriptions sync.
*
* @param immediately YES if the sync should be scheduled immediately else NO if we can delay
* the sync.
*/
- (void)scheduleSync:(BOOL)immediately;
/**
* Adds the "/topics/" prefix to the topic.
*
* @param topic The topic to add the prefix to.
*
* @return The new topic name with the "/topics/" prefix added.
*/
+ (NSString *)addPrefixToTopic:(NSString *)topic;
/**
* Removes the "/topics/" prefix from the topic.
*
* @param topic The topic to remove the prefix from.
*
* @return The new topic name with the "/topics/" prefix removed.
*/
+ (NSString *)removePrefixFromTopic:(NSString *)topic;
/**
* Check if the topic name has "/topics/" prefix.
*
* @param topic The topic name to verify.
*
* @return YES if the topic name has "/topics/" prefix else NO.
*/
+ (BOOL)hasTopicsPrefix:(NSString *)topic;
/**
* Check if it's a valid topic name. This includes "/topics/" prefix in the topic name.
*
* @param topic The topic name to verify.
*
* @return YES if the topic name satisfies the regex "/topics/[a-zA-Z0-9-_.~%]{1,900}".
*/
+ (BOOL)isValidTopicWithPrefix:(NSString *)topic;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,288 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessagingPubSub.h"
#import <GoogleUtilities/GULUserDefaults.h>
#import "FIRMessaging.h"
#import "FIRMessagingClient.h"
#import "FIRMessagingDefines.h"
#import "FIRMessagingLogger.h"
#import "FIRMessagingPendingTopicsList.h"
#import "FIRMessagingUtilities.h"
#import "FIRMessaging_Private.h"
#import "NSDictionary+FIRMessaging.h"
#import "NSError+FIRMessaging.h"
static NSString *const kPendingSubscriptionsListKey =
@"com.firebase.messaging.pending-subscriptions";
@interface FIRMessagingPubSub () <FIRMessagingPendingTopicsListDelegate>
@property(nonatomic, readwrite, strong) FIRMessagingPendingTopicsList *pendingTopicUpdates;
@property(nonatomic, readwrite, strong) FIRMessagingClient *client;
@end
@implementation FIRMessagingPubSub
- (instancetype)init {
FIRMessagingInvalidateInitializer();
// Need this to disable an Xcode warning.
return [self initWithClient:nil];
}
- (instancetype)initWithClient:(FIRMessagingClient *)client {
self = [super init];
if (self) {
_client = client;
[self restorePendingTopicsList];
}
return self;
}
- (void)subscribeWithToken:(NSString *)token
topic:(NSString *)topic
options:(NSDictionary *)options
handler:(FIRMessagingTopicOperationCompletion)handler {
_FIRMessagingDevAssert([token length], @"FIRMessaging error no token specified");
_FIRMessagingDevAssert([topic length], @"FIRMessaging error Invalid empty topic specified");
if (!self.client) {
handler([NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubFIRMessagingNotSetup]);
return;
}
token = [token copy];
topic = [topic copy];
if (![options count]) {
options = @{};
}
if (![[self class] isValidTopicWithPrefix:topic]) {
FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub000,
@"Invalid FIRMessaging Pubsub topic %@", topic);
handler([NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubInvalidTopic]);
return;
}
if (![self verifyPubSubOptions:options]) {
// we do not want to quit even if options have some invalid values.
FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub001,
@"Invalid options passed to FIRMessagingPubSub with non-string keys or "
"values.");
}
// copy the dictionary would trim non-string keys or values if any.
options = [options fcm_trimNonStringValues];
[self.client updateSubscriptionWithToken:token
topic:topic
options:options
shouldDelete:NO
handler:^void(NSError *error) {
handler(error);
}];
}
- (void)unsubscribeWithToken:(NSString *)token
topic:(NSString *)topic
options:(NSDictionary *)options
handler:(FIRMessagingTopicOperationCompletion)handler {
_FIRMessagingDevAssert([token length], @"FIRMessaging error no token specified");
_FIRMessagingDevAssert([topic length], @"FIRMessaging error Invalid empty topic specified");
if (!self.client) {
handler([NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubFIRMessagingNotSetup]);
return;
}
token = [token copy];
topic = [topic copy];
if (![options count]) {
options = @{};
}
if (![[self class] isValidTopicWithPrefix:topic]) {
FIRMessagingLoggerError(kFIRMessagingMessageCodePubSub002,
@"Invalid FIRMessaging Pubsub topic %@", topic);
handler([NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubInvalidTopic]);
return;
}
if (![self verifyPubSubOptions:options]) {
// we do not want to quit even if options have some invalid values.
FIRMessagingLoggerError(
kFIRMessagingMessageCodePubSub003,
@"Invalid options passed to FIRMessagingPubSub with non-string keys or values.");
}
// copy the dictionary would trim non-string keys or values if any.
options = [options fcm_trimNonStringValues];
[self.client updateSubscriptionWithToken:token
topic:topic
options:options
shouldDelete:YES
handler:^void(NSError *error) {
handler(error);
}];
}
- (void)subscribeToTopic:(NSString *)topic
handler:(nullable FIRMessagingTopicOperationCompletion)handler {
[self.pendingTopicUpdates addOperationForTopic:topic
withAction:FIRMessagingTopicActionSubscribe
completion:handler];
}
- (void)unsubscribeFromTopic:(NSString *)topic
handler:(nullable FIRMessagingTopicOperationCompletion)handler {
[self.pendingTopicUpdates addOperationForTopic:topic
withAction:FIRMessagingTopicActionUnsubscribe
completion:handler];
}
- (void)scheduleSync:(BOOL)immediately {
NSString *fcmToken = [[FIRMessaging messaging] defaultFcmToken];
if (fcmToken.length) {
[self.pendingTopicUpdates resumeOperationsIfNeeded];
}
}
#pragma mark - FIRMessagingPendingTopicsListDelegate
- (void)pendingTopicsList:(FIRMessagingPendingTopicsList *)list
requestedUpdateForTopic:(NSString *)topic
action:(FIRMessagingTopicAction)action
completion:(FIRMessagingTopicOperationCompletion)completion {
NSString *fcmToken = [[FIRMessaging messaging] defaultFcmToken];
if (action == FIRMessagingTopicActionSubscribe) {
[self subscribeWithToken:fcmToken topic:topic options:nil handler:completion];
} else {
[self unsubscribeWithToken:fcmToken topic:topic options:nil handler:completion];
}
}
- (void)pendingTopicsListDidUpdate:(FIRMessagingPendingTopicsList *)list {
[self archivePendingTopicsList:list];
}
- (BOOL)pendingTopicsListCanRequestTopicUpdates:(FIRMessagingPendingTopicsList *)list {
NSString *fcmToken = [[FIRMessaging messaging] defaultFcmToken];
return (fcmToken.length > 0);
}
#pragma mark - Storing Pending Topics
- (void)archivePendingTopicsList:(FIRMessagingPendingTopicsList *)topicsList {
GULUserDefaults *defaults = [GULUserDefaults standardUserDefaults];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
NSData *pendingData = [NSKeyedArchiver archivedDataWithRootObject:topicsList];
#pragma clang diagnostic pop
[defaults setObject:pendingData forKey:kPendingSubscriptionsListKey];
[defaults synchronize];
}
- (void)restorePendingTopicsList {
GULUserDefaults *defaults = [GULUserDefaults standardUserDefaults];
NSData *pendingData = [defaults objectForKey:kPendingSubscriptionsListKey];
FIRMessagingPendingTopicsList *subscriptions;
@try {
if (pendingData) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
subscriptions = [NSKeyedUnarchiver unarchiveObjectWithData:pendingData];
#pragma clang diagnostic pop
}
} @catch (NSException *exception) {
// Nothing we can do, just continue as if we don't have pending subscriptions
} @finally {
if (subscriptions) {
self.pendingTopicUpdates = subscriptions;
} else {
self.pendingTopicUpdates = [[FIRMessagingPendingTopicsList alloc] init];
}
self.pendingTopicUpdates.delegate = self;
}
}
#pragma mark - Private Helpers
- (BOOL)verifyPubSubOptions:(NSDictionary *)options {
return ![options fcm_hasNonStringKeysOrValues];
}
#pragma mark - Topic Name Helpers
static NSString *const kTopicsPrefix = @"/topics/";
static NSString *const kTopicRegexPattern = @"/topics/([a-zA-Z0-9-_.~%]+)";
+ (NSString *)addPrefixToTopic:(NSString *)topic {
if (![self hasTopicsPrefix:topic]) {
return [NSString stringWithFormat:@"%@%@", kTopicsPrefix, topic];
} else {
return [topic copy];
}
}
+ (NSString *)removePrefixFromTopic:(NSString *)topic {
if ([self hasTopicsPrefix:topic]) {
return [topic substringFromIndex:kTopicsPrefix.length];
} else {
return [topic copy];
}
}
+ (BOOL)hasTopicsPrefix:(NSString *)topic {
return [topic hasPrefix:kTopicsPrefix];
}
/**
* Returns a regular expression for matching a topic sender.
*
* @return The topic matching regular expression
*/
+ (NSRegularExpression *)topicRegex {
// Since this is a static regex pattern, we only only need to declare it once.
static NSRegularExpression *topicRegex;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSError *error;
topicRegex =
[NSRegularExpression regularExpressionWithPattern:kTopicRegexPattern
options:NSRegularExpressionAnchorsMatchLines
error:&error];
});
return topicRegex;
}
/**
* Gets the class describing occurences of topic names and sender IDs in the sender.
*
* @param expression The topic expression used to generate a pubsub topic
*
* @return Representation of captured subexpressions in topic regular expression
*/
+ (BOOL)isValidTopicWithPrefix:(NSString *)topic {
NSRange topicRange = NSMakeRange(0, topic.length);
NSRange regexMatchRange = [[self topicRegex] rangeOfFirstMatchInString:topic
options:NSMatchingAnchored
range:topicRange];
return NSEqualRanges(topicRange, regexMatchRange);
}
@end
@@ -0,0 +1,44 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessagingTopicOperation.h"
@interface FIRMessagingPubSubRegistrar : NSObject
/**
* Stops all the subscription requests going on in parallel. This would
* invalidate all the handlers associated with the subscription requests.
*/
- (void)stopAllSubscriptionRequests;
/**
* Update subscription status for a given topic with FIRMessaging's backend.
*
* @param topic The topic to subscribe to.
* @param token The registration token to be used.
* @param options The options to be passed in during subscription request.
* @param shouldDelete NO if the subscription is being added else YES if being
* removed.
* @param handler The handler invoked once the update subscription request
* finishes.
*/
- (void)updateSubscriptionToTopic:(NSString *)topic
withToken:(NSString *)token
options:(NSDictionary *)options
shouldDelete:(BOOL)shouldDelete
handler:(FIRMessagingTopicOperationCompletion)handler;
@end
@@ -0,0 +1,69 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessagingPubSubRegistrar.h"
#import "FIRMessagingDefines.h"
#import "FIRMessagingPubSubRegistrar.h"
#import "FIRMessagingTopicsCommon.h"
#import "NSError+FIRMessaging.h"
@interface FIRMessagingPubSubRegistrar ()
@property(nonatomic, readonly, strong) NSOperationQueue *topicOperations;
// Common errors, instantiated, to avoid generating multiple copies
@property(nonatomic, readwrite, strong) NSError *operationInProgressError;
@end
@implementation FIRMessagingPubSubRegistrar
- (instancetype)init {
self = [super init];
if (self) {
_topicOperations = [[NSOperationQueue alloc] init];
// Do 10 topic operations at a time; it's enough to keep the TCP connection to the host alive,
// saving hundreds of milliseconds on each request (compared to a serial queue).
_topicOperations.maxConcurrentOperationCount = 10;
}
return self;
}
- (void)stopAllSubscriptionRequests {
[self.topicOperations cancelAllOperations];
}
- (void)updateSubscriptionToTopic:(NSString *)topic
withToken:(NSString *)token
options:(NSDictionary *)options
shouldDelete:(BOOL)shouldDelete
handler:(FIRMessagingTopicOperationCompletion)handler {
FIRMessagingTopicAction action = FIRMessagingTopicActionSubscribe;
if (shouldDelete) {
action = FIRMessagingTopicActionUnsubscribe;
}
FIRMessagingTopicOperation *operation =
[[FIRMessagingTopicOperation alloc] initWithTopic:topic
action:action
token:token
options:options
completion:handler];
[self.topicOperations addOperation:operation];
}
@end
@@ -0,0 +1,36 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessagingDataMessageManager.h"
#import "FIRMessaging.h"
NS_ASSUME_NONNULL_BEGIN
@class FIRMessagingReceiver;
@protocol FIRMessagingReceiverDelegate <NSObject>
- (void)receiver:(FIRMessagingReceiver *)receiver
receivedRemoteMessage:(FIRMessagingRemoteMessage *)remoteMessage;
@end
@interface FIRMessagingReceiver : NSObject <FIRMessagingDataMessageManagerDelegate>
@property(nonatomic, weak, nullable) id<FIRMessagingReceiverDelegate> delegate;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,97 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessagingReceiver.h"
#import "FIRMessaging.h"
#import "FIRMessagingLogger.h"
#import "FIRMessagingUtilities.h"
#import "FIRMessaging_Private.h"
static NSString *const kUpstreamMessageIDUserInfoKey = @"messageID";
static NSString *const kUpstreamErrorUserInfoKey = @"error";
static int downstreamMessageID = 0;
@implementation FIRMessagingReceiver
#pragma mark - FIRMessagingDataMessageManager protocol
- (void)didReceiveMessage:(NSDictionary *)message withIdentifier:(nullable NSString *)messageID {
if (![messageID length]) {
messageID = [[self class] nextMessageID];
}
[self handleDirectChannelMessage:message withIdentifier:messageID];
}
- (void)willSendDataMessageWithID:(NSString *)messageID error:(NSError *)error {
NSNotification *notification;
if (error) {
NSDictionary *userInfo = @{
kUpstreamMessageIDUserInfoKey : [messageID copy],
kUpstreamErrorUserInfoKey : error
};
notification = [NSNotification notificationWithName:FIRMessagingSendErrorNotification
object:nil
userInfo:userInfo];
[[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostASAP];
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeReceiver000,
@"Fail to send upstream message: %@ error: %@", messageID, error);
} else {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeReceiver001, @"Will send upstream message: %@",
messageID);
}
}
- (void)didSendDataMessageWithID:(NSString *)messageID {
// invoke the callbacks asynchronously
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeReceiver002, @"Did send upstream message: %@",
messageID);
NSNotification * notification =
[NSNotification notificationWithName:FIRMessagingSendSuccessNotification
object:nil
userInfo:@{ kUpstreamMessageIDUserInfoKey : [messageID copy] }];
[[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostASAP];
}
- (void)didDeleteMessagesOnServer {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeReceiver003,
@"Will send deleted messages notification");
NSNotification * notification =
[NSNotification notificationWithName:FIRMessagingMessagesDeletedNotification
object:nil];
[[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostASAP];
}
#pragma mark - Private Helpers
- (void)handleDirectChannelMessage:(NSDictionary *)message withIdentifier:(NSString *)messageID {
FIRMessagingRemoteMessage *wrappedMessage = [[FIRMessagingRemoteMessage alloc] init];
wrappedMessage.appData = [message copy];
wrappedMessage.messageID = messageID;
[self.delegate receiver:self receivedRemoteMessage:wrappedMessage];
}
+ (NSString *)nextMessageID {
@synchronized (self) {
++downstreamMessageID;
return [NSString stringWithFormat:@"gcm-%d", downstreamMessageID];
}
}
@end
@@ -0,0 +1,45 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
/**
* Swizzle remote-notification callbacks to invoke FIRMessaging methods
* before calling original implementations.
*/
@interface FIRMessagingRemoteNotificationsProxy : NSObject
/**
* Checks the `FirebaseAppDelegateProxyEnabled` key in the App's Info.plist. If the key is
* missing or incorrectly formatted, returns `YES`.
*
* @return YES if the Application Delegate and User Notification Center methods can be swizzled.
* Otherwise, returns NO.
*/
+ (BOOL)canSwizzleMethods;
/**
* A shared instance of `FIRMessagingRemoteNotificationsProxy`
*/
+ (instancetype)sharedProxy;
/**
* Swizzles Application Delegate's remote-notification callbacks and User Notification Center
* delegate callback, and invokes the original selectors once done.
*/
- (void)swizzleMethodsIfPossible;
@end
@@ -0,0 +1,602 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessagingRemoteNotificationsProxy.h"
#import <objc/runtime.h>
#import "FIRMessagingConstants.h"
#import "FIRMessagingLogger.h"
#import "FIRMessagingUtilities.h"
#import "FIRMessaging_Private.h"
#import <GoogleUtilities/GULAppDelegateSwizzler.h>
static void * UserNotificationObserverContext = &UserNotificationObserverContext;
static NSString *kUserNotificationWillPresentSelectorString =
@"userNotificationCenter:willPresentNotification:withCompletionHandler:";
static NSString *kUserNotificationDidReceiveResponseSelectorString =
@"userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:";
@interface FIRMessagingRemoteNotificationsProxy () <GULApplicationDelegate>
@property(strong, nonatomic) NSMutableDictionary<NSString *, NSValue *> *originalAppDelegateImps;
@property(strong, nonatomic) NSMutableDictionary<NSString *, NSArray *> *swizzledSelectorsByClass;
@property(nonatomic) BOOL didSwizzleMethods;
@property(nonatomic) BOOL hasSwizzledUserNotificationDelegate;
@property(nonatomic) BOOL isObservingUserNotificationDelegateChanges;
@property(strong, nonatomic) id userNotificationCenter;
@property(strong, nonatomic) id currentUserNotificationCenterDelegate;
@property(strong, nonatomic) GULAppDelegateInterceptorID appDelegateInterceptorID;
@end
@implementation FIRMessagingRemoteNotificationsProxy
+ (BOOL)canSwizzleMethods {
return [GULAppDelegateSwizzler isAppDelegateProxyEnabled];
}
+ (instancetype)sharedProxy {
static FIRMessagingRemoteNotificationsProxy *proxy;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
proxy = [[FIRMessagingRemoteNotificationsProxy alloc] init];
});
return proxy;
}
- (instancetype)init {
self = [super init];
if (self) {
_originalAppDelegateImps = [[NSMutableDictionary alloc] init];
_swizzledSelectorsByClass = [[NSMutableDictionary alloc] init];
}
return self;
}
- (void)dealloc {
[self unswizzleAllMethods];
self.swizzledSelectorsByClass = nil;
[self.originalAppDelegateImps removeAllObjects];
self.originalAppDelegateImps = nil;
[self removeUserNotificationCenterDelegateObserver];
}
- (void)swizzleMethodsIfPossible {
// Already swizzled.
if (self.didSwizzleMethods) {
return;
}
[GULAppDelegateSwizzler proxyOriginalDelegateIncludingAPNSMethods];
self.appDelegateInterceptorID = [GULAppDelegateSwizzler registerAppDelegateInterceptor:self];
// Add KVO listener on [UNUserNotificationCenter currentNotificationCenter]'s delegate property
Class notificationCenterClass = NSClassFromString(@"UNUserNotificationCenter");
if (notificationCenterClass) {
// We are linked against iOS 10 SDK or above
id notificationCenter = getNamedPropertyFromObject(notificationCenterClass,
@"currentNotificationCenter",
notificationCenterClass);
if (notificationCenter) {
[self listenForDelegateChangesInUserNotificationCenter:notificationCenter];
}
}
self.didSwizzleMethods = YES;
}
- (void)unswizzleAllMethods {
if (self.appDelegateInterceptorID) {
[GULAppDelegateSwizzler unregisterAppDelegateInterceptorWithID:self.appDelegateInterceptorID];
}
for (NSString *className in self.swizzledSelectorsByClass) {
Class klass = NSClassFromString(className);
NSArray *selectorStrings = self.swizzledSelectorsByClass[className];
for (NSString *selectorString in selectorStrings) {
SEL selector = NSSelectorFromString(selectorString);
[self unswizzleSelector:selector inClass:klass];
}
}
[self.swizzledSelectorsByClass removeAllObjects];
}
- (void)listenForDelegateChangesInUserNotificationCenter:(id)notificationCenter {
Class notificationCenterClass = NSClassFromString(@"UNUserNotificationCenter");
if (![notificationCenter isKindOfClass:notificationCenterClass]) {
return;
}
id delegate = getNamedPropertyFromObject(notificationCenter, @"delegate", nil);
Protocol *delegateProtocol = NSProtocolFromString(@"UNUserNotificationCenterDelegate");
if ([delegate conformsToProtocol:delegateProtocol]) {
// Swizzle this object now, if available
[self swizzleUserNotificationCenterDelegate:delegate];
}
// Add KVO observer for "delegate" keyPath for future changes
[self addDelegateObserverToUserNotificationCenter:notificationCenter];
}
#pragma mark - UNNotificationCenter Swizzling
- (void)swizzleUserNotificationCenterDelegate:(id _Nonnull)delegate {
if (self.currentUserNotificationCenterDelegate == delegate) {
// Via pointer-check, compare if we have already swizzled this item.
return;
}
Protocol *userNotificationCenterProtocol =
NSProtocolFromString(@"UNUserNotificationCenterDelegate");
if ([delegate conformsToProtocol:userNotificationCenterProtocol]) {
SEL willPresentNotificationSelector =
NSSelectorFromString(kUserNotificationWillPresentSelectorString);
// Swizzle the optional method
// "userNotificationCenter:willPresentNotification:withCompletionHandler:", if it is
// implemented. Do not swizzle otherwise, as an implementation *will* be created, which will
// fool iOS into thinking that this method is implemented, and therefore not send notifications
// to the fallback method in the app delegate
// "application:didReceiveRemoteNotification:fetchCompletionHandler:".
if ([delegate respondsToSelector:willPresentNotificationSelector]) {
[self swizzleSelector:willPresentNotificationSelector
inClass:[delegate class]
withImplementation:(IMP)FCM_swizzle_willPresentNotificationWithHandler
inProtocol:userNotificationCenterProtocol];
}
SEL didReceiveNotificationResponseSelector =
NSSelectorFromString(kUserNotificationDidReceiveResponseSelectorString);
if ([delegate respondsToSelector:didReceiveNotificationResponseSelector]) {
[self swizzleSelector:didReceiveNotificationResponseSelector
inClass:[delegate class]
withImplementation:(IMP)FCM_swizzle_didReceiveNotificationResponseWithHandler
inProtocol:userNotificationCenterProtocol];
}
self.currentUserNotificationCenterDelegate = delegate;
self.hasSwizzledUserNotificationDelegate = YES;
}
}
- (void)unswizzleUserNotificationCenterDelegate:(id _Nonnull)delegate {
if (self.currentUserNotificationCenterDelegate != delegate) {
// We aren't swizzling this delegate, so don't do anything.
return;
}
SEL willPresentNotificationSelector =
NSSelectorFromString(kUserNotificationWillPresentSelectorString);
// Call unswizzle methods, even if the method was not implemented (it will fail gracefully).
[self unswizzleSelector:willPresentNotificationSelector
inClass:[self.currentUserNotificationCenterDelegate class]];
SEL didReceiveNotificationResponseSelector =
NSSelectorFromString(kUserNotificationDidReceiveResponseSelectorString);
[self unswizzleSelector:didReceiveNotificationResponseSelector
inClass:[self.currentUserNotificationCenterDelegate class]];
self.currentUserNotificationCenterDelegate = nil;
self.hasSwizzledUserNotificationDelegate = NO;
}
#pragma mark - KVO for UNUserNotificationCenter
- (void)addDelegateObserverToUserNotificationCenter:(id)userNotificationCenter {
[self removeUserNotificationCenterDelegateObserver];
@try {
[userNotificationCenter addObserver:self
forKeyPath:NSStringFromSelector(@selector(delegate))
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:UserNotificationObserverContext];
self.userNotificationCenter = userNotificationCenter;
self.isObservingUserNotificationDelegateChanges = YES;
} @catch (NSException *exception) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeRemoteNotificationsProxy000,
@"Encountered exception trying to add a KVO observer for "
@"UNUserNotificationCenter's 'delegate' property: %@",
exception);
} @finally {
}
}
- (void)removeUserNotificationCenterDelegateObserver {
if (!self.userNotificationCenter) {
return;
}
@try {
[self.userNotificationCenter removeObserver:self
forKeyPath:NSStringFromSelector(@selector(delegate))
context:UserNotificationObserverContext];
self.userNotificationCenter = nil;
self.isObservingUserNotificationDelegateChanges = NO;
} @catch (NSException *exception) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeRemoteNotificationsProxy001,
@"Encountered exception trying to remove a KVO observer for "
@"UNUserNotificationCenter's 'delegate' property: %@",
exception);
} @finally {
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey, id> *)change
context:(void *)context {
if (context == UserNotificationObserverContext) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(delegate))]) {
id oldDelegate = change[NSKeyValueChangeOldKey];
if (oldDelegate && oldDelegate != [NSNull null]) {
[self unswizzleUserNotificationCenterDelegate:oldDelegate];
}
id newDelegate = change[NSKeyValueChangeNewKey];
if (newDelegate && newDelegate != [NSNull null]) {
[self swizzleUserNotificationCenterDelegate:newDelegate];
}
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
#pragma mark - NSProxy methods
- (void)saveOriginalImplementation:(IMP)imp forSelector:(SEL)selector {
if (imp && selector) {
NSValue *IMPValue = [NSValue valueWithPointer:imp];
NSString *selectorString = NSStringFromSelector(selector);
self.originalAppDelegateImps[selectorString] = IMPValue;
}
}
- (IMP)originalImplementationForSelector:(SEL)selector {
NSString *selectorString = NSStringFromSelector(selector);
NSValue *implementation_value = self.originalAppDelegateImps[selectorString];
if (!implementation_value) {
return nil;
}
IMP imp;
[implementation_value getValue:&imp];
return imp;
}
- (void)trackSwizzledSelector:(SEL)selector ofClass:(Class)klass {
NSString *className = NSStringFromClass(klass);
NSString *selectorString = NSStringFromSelector(selector);
NSArray *selectors = self.swizzledSelectorsByClass[selectorString];
if (selectors) {
selectors = [selectors arrayByAddingObject:selectorString];
} else {
selectors = @[selectorString];
}
self.swizzledSelectorsByClass[className] = selectors;
}
- (void)removeImplementationForSelector:(SEL)selector {
NSString *selectorString = NSStringFromSelector(selector);
[self.originalAppDelegateImps removeObjectForKey:selectorString];
}
- (void)swizzleSelector:(SEL)originalSelector
inClass:(Class)klass
withImplementation:(IMP)swizzledImplementation
inProtocol:(Protocol *)protocol {
Method originalMethod = class_getInstanceMethod(klass, originalSelector);
if (originalMethod) {
// This class implements this method, so replace the original implementation
// with our new implementation and save the old implementation.
IMP __original_method_implementation =
method_setImplementation(originalMethod, swizzledImplementation);
IMP __nonexistant_method_implementation = [self nonExistantMethodImplementationForClass:klass];
if (__original_method_implementation &&
__original_method_implementation != __nonexistant_method_implementation &&
__original_method_implementation != swizzledImplementation) {
[self saveOriginalImplementation:__original_method_implementation
forSelector:originalSelector];
}
} else {
// The class doesn't have this method, so add our swizzled implementation as the
// original implementation of the original method.
struct objc_method_description method_description =
protocol_getMethodDescription(protocol, originalSelector, NO, YES);
BOOL methodAdded = class_addMethod(klass,
originalSelector,
swizzledImplementation,
method_description.types);
if (!methodAdded) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeRemoteNotificationsProxyMethodNotAdded,
@"Could not add method for %@ to class %@",
NSStringFromSelector(originalSelector),
NSStringFromClass(klass));
}
}
[self trackSwizzledSelector:originalSelector ofClass:klass];
}
- (void)unswizzleSelector:(SEL)selector inClass:(Class)klass {
Method swizzledMethod = class_getInstanceMethod(klass, selector);
if (!swizzledMethod) {
// This class doesn't seem to have this selector as an instance method? Bail out.
return;
}
IMP original_imp = [self originalImplementationForSelector:selector];
if (original_imp) {
// Restore the original implementation as the current implementation
method_setImplementation(swizzledMethod, original_imp);
[self removeImplementationForSelector:selector];
} else {
// This class originally did not have an implementation for this selector.
// We can't actually remove methods in Objective C 2.0, but we could set
// its method to something non-existent. This should give us the same
// behavior as if the method was not implemented.
// See: http://stackoverflow.com/a/8276527/9849
IMP nonExistantMethodImplementation = [self nonExistantMethodImplementationForClass:klass];
method_setImplementation(swizzledMethod, nonExistantMethodImplementation);
}
}
#pragma mark - Reflection Helpers
// This is useful to generate from a stable, "known missing" selector, as the IMP can be compared
// in case we are setting an implementation for a class that was previously "unswizzled" into a
// non-existant implementation.
- (IMP)nonExistantMethodImplementationForClass:(Class)klass {
SEL nonExistantSelector = NSSelectorFromString(@"aNonExistantMethod");
IMP nonExistantMethodImplementation = class_getMethodImplementation(klass, nonExistantSelector);
return nonExistantMethodImplementation;
}
// A safe, non-leaky way return a property object by its name
id getNamedPropertyFromObject(id object, NSString *propertyName, Class klass) {
SEL selector = NSSelectorFromString(propertyName);
if (![object respondsToSelector:selector]) {
return nil;
}
if (!klass) {
klass = [NSObject class];
}
// Suppress clang warning about leaks in performSelector
// The alternative way to perform this is to invoke
// the method as a block (see http://stackoverflow.com/a/20058585),
// but this approach sometimes returns incomplete objects.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
id property = [object performSelector:selector];
#pragma clang diagnostic pop
if (![property isKindOfClass:klass]) {
return nil;
}
return property;
}
#pragma mark - GULApplicationDelegate
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
- (void)application:(GULApplication *)application
didReceiveRemoteNotification:(NSDictionary *)userInfo {
[[FIRMessaging messaging] appDidReceiveMessage:userInfo];
}
#pragma clang diagnostic pop
#if TARGET_OS_IOS || TARGET_OS_TV
- (void)application:(UIApplication *)application
didReceiveRemoteNotification:(NSDictionary *)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
[[FIRMessaging messaging] appDidReceiveMessage:userInfo];
}
- (void)application:(UIApplication *)application
didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
// Log the fact that we failed to register for remote notifications
FIRMessagingLoggerError(kFIRMessagingMessageCodeRemoteNotificationsProxyAPNSFailed,
@"Error in "
@"application:didFailToRegisterForRemoteNotificationsWithError: %@",
error.localizedDescription);
}
#endif
- (void)application:(GULApplication *)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
[FIRMessaging messaging].APNSToken = deviceToken;
}
#pragma mark - Swizzled Methods
/**
* Swizzle the notification handler for iOS 10+ devices.
* Signature of original handler is as below:
* - (void)userNotificationCenter:(UNUserNotificationCenter *)center
* willPresentNotification:(UNNotification *)notification
* withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
* In order to make FCM SDK compile and compatible with iOS SDKs before iOS 10, hide the
* parameter types from the swizzling implementation.
*/
void FCM_swizzle_willPresentNotificationWithHandler(
id self, SEL _cmd, id center, id notification, void (^handler)(NSUInteger)) {
FIRMessagingRemoteNotificationsProxy *proxy = [FIRMessagingRemoteNotificationsProxy sharedProxy];
IMP original_imp = [proxy originalImplementationForSelector:_cmd];
void (^callOriginalMethodIfAvailable)(void) = ^{
if (original_imp) {
((void (*)(id, SEL, id, id, void (^)(NSUInteger)))original_imp)(
self, _cmd, center, notification, handler);
}
return;
};
Class notificationCenterClass = NSClassFromString(@"UNUserNotificationCenter");
Class notificationClass = NSClassFromString(@"UNNotification");
if (!notificationCenterClass || !notificationClass) {
// Can't find UserNotifications framework. Do not swizzle, just execute the original method.
callOriginalMethodIfAvailable();
}
if (!center || ![center isKindOfClass:[notificationCenterClass class]]) {
// Invalid parameter type from the original method.
// Do not swizzle, just execute the original method.
callOriginalMethodIfAvailable();
return;
}
if (!notification || ![notification isKindOfClass:[notificationClass class]]) {
// Invalid parameter type from the original method.
// Do not swizzle, just execute the original method.
callOriginalMethodIfAvailable();
return;
}
if (!handler) {
// Invalid parameter type from the original method.
// Do not swizzle, just execute the original method.
callOriginalMethodIfAvailable();
return;
}
// Attempt to access the user info
id notificationUserInfo = userInfoFromNotification(notification);
if (!notificationUserInfo) {
// Could not access notification.request.content.userInfo.
callOriginalMethodIfAvailable();
return;
}
[[FIRMessaging messaging] appDidReceiveMessage:notificationUserInfo];
// Execute the original implementation.
callOriginalMethodIfAvailable();
}
/**
* Swizzle the notification handler for iOS 10+ devices.
* Signature of original handler is as below:
* - (void)userNotificationCenter:(UNUserNotificationCenter *)center
* didReceiveNotificationResponse:(UNNotificationResponse *)response
* withCompletionHandler:(void (^)(void))completionHandler
* In order to make FCM SDK compile and compatible with iOS SDKs before iOS 10, hide the
* parameter types from the swizzling implementation.
*/
void FCM_swizzle_didReceiveNotificationResponseWithHandler(
id self, SEL _cmd, id center, id response, void (^handler)(void)) {
FIRMessagingRemoteNotificationsProxy *proxy = [FIRMessagingRemoteNotificationsProxy sharedProxy];
IMP original_imp = [proxy originalImplementationForSelector:_cmd];
void (^callOriginalMethodIfAvailable)(void) = ^{
if (original_imp) {
((void (*)(id, SEL, id, id, void (^)(void)))original_imp)(
self, _cmd, center, response, handler);
}
return;
};
Class notificationCenterClass = NSClassFromString(@"UNUserNotificationCenter");
Class responseClass = NSClassFromString(@"UNNotificationResponse");
if (!center || ![center isKindOfClass:[notificationCenterClass class]]) {
// Invalid parameter type from the original method.
// Do not swizzle, just execute the original method.
callOriginalMethodIfAvailable();
return;
}
if (!response || ![response isKindOfClass:[responseClass class]]) {
// Invalid parameter type from the original method.
// Do not swizzle, just execute the original method.
callOriginalMethodIfAvailable();
return;
}
if (!handler) {
// Invalid parameter type from the original method.
// Do not swizzle, just execute the original method.
callOriginalMethodIfAvailable();
return;
}
// Try to access the response.notification property
SEL notificationSelector = NSSelectorFromString(@"notification");
if (![response respondsToSelector:notificationSelector]) {
// Cannot access the .notification property.
callOriginalMethodIfAvailable();
return;
}
id notificationClass = NSClassFromString(@"UNNotification");
id notification = getNamedPropertyFromObject(response, @"notification", notificationClass);
// With a notification object, use the common code to reach deep into notification
// (notification.request.content.userInfo)
id notificationUserInfo = userInfoFromNotification(notification);
if (!notificationUserInfo) {
// Could not access notification.request.content.userInfo.
callOriginalMethodIfAvailable();
return;
}
[[FIRMessaging messaging] appDidReceiveMessage:notificationUserInfo];
// Execute the original implementation.
callOriginalMethodIfAvailable();
}
id userInfoFromNotification(id notification) {
// Select the userInfo field from UNNotification.request.content.userInfo.
SEL requestSelector = NSSelectorFromString(@"request");
if (![notification respondsToSelector:requestSelector]) {
// Cannot access the request property.
return nil;
}
Class requestClass = NSClassFromString(@"UNNotificationRequest");
id notificationRequest = getNamedPropertyFromObject(notification, @"request", requestClass);
SEL notificationContentSelector = NSSelectorFromString(@"content");
if (!notificationRequest
|| ![notificationRequest respondsToSelector:notificationContentSelector]) {
// Cannot access the content property.
return nil;
}
Class contentClass = NSClassFromString(@"UNNotificationContent");
id notificationContent = getNamedPropertyFromObject(notificationRequest,
@"content",
contentClass);
SEL notificationUserInfoSelector = NSSelectorFromString(@"userInfo");
if (!notificationContent
|| ![notificationContent respondsToSelector:notificationUserInfoSelector]) {
// Cannot access the userInfo property.
return nil;
}
id notificationUserInfo = getNamedPropertyFromObject(notificationContent,
@"userInfo",
[NSDictionary class]);
if (!notificationUserInfo) {
// This is not the expected notification handler.
return nil;
}
return notificationUserInfo;
}
@end
@@ -0,0 +1,201 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
@class FIRMessagingPersistentSyncMessage;
// table data handlers
/**
* Handle message stored in the outgoing RMQ messages table.
*
* @param rmqId The rmqID of the message.
* @param tag The message tag.
* @param data The data stored in the message.
*/
typedef void(^FCMOutgoingRmqMessagesTableHandler)(int64_t rmqId, int8_t tag, NSData *data);
/// Outgoing messages RMQ table
extern NSString *const kTableOutgoingRmqMessages;
/// Server to device RMQ table
extern NSString *const kTableS2DRmqIds;
@interface FIRMessagingRmq2PersistentStore : NSObject
/**
* Initialize and open the RMQ database on the client.
*
* @param databaseName The name for RMQ database.
*
* @return A store used to persist messages on the client.
*/
- (instancetype)initWithDatabaseName:(NSString *)databaseName;
/**
* Save outgoing message in RMQ.
*
* @param rmqId The rmqID for the message.
* @param tag The tag of the message proto.
* @param data The data being sent in the message.
* @param error The error if any while saving the message to the persistent store.
*
* @return YES if the message was successfully saved to the persistent store else NO.
*/
- (BOOL)saveMessageWithRmqId:(int64_t)rmqId
tag:(int8_t)tag
data:(NSData *)data
error:(NSError **)error;
/**
* Add unacked server to device message with a given rmqID to the persistent store.
*
* @param rmqId The rmqID of the message that was not acked by the cient.
*
* @return YES if the save was successful else NO.
*/
- (BOOL)saveUnackedS2dMessageWithRmqId:(NSString *)rmqId;
/**
* Update the last RMQ ID that was sent by the client.
*
* @param rmqID The latest rmqID sent by the device.
*
* @return YES if the last rmqID was successfully saved else NO.
*/
- (BOOL)updateLastOutgoingRmqId:(int64_t)rmqID;
#pragma mark - Query
/**
* Query the highest rmqID saved in the Outgoing messages table.
*
* @return The highest rmqID amongst all the messages in the Outgoing RMQ table. If no message
* was ever persisted return 0.
*/
- (int64_t)queryHighestRmqId;
/**
* The last rmqID that was saved on the client.
*
* @return The last rmqID that was saved. If no rmqID was ever persisted return 0.
*/
- (int64_t)queryLastRmqId;
/**
* Get a list of all unacked server to device messages stored on the client.
*
* @return List of all unacked s2d messages in the persistent store.
*/
- (NSArray *)unackedS2dRmqIds;
/**
* Iterate over all outgoing messages in the RMQ table.
*
* @param handler The handler invoked with each message in the outgoing RMQ table.
*/
- (void)scanOutgoingRmqMessagesWithHandler:(FCMOutgoingRmqMessagesTableHandler)handler;
#pragma mark - Delete
/**
* Delete messages with given rmqID's from a table.
*
* @param tableName The table name from which to delete the rmq messages.
* @param rmqIds The rmqID's of the messages to be deleted.
*
* @return The number of messages that were successfully deleted.
*/
- (int)deleteMessagesFromTable:(NSString *)tableName
withRmqIds:(NSArray *)rmqIds;
/**
* Remove database from the device.
*
* @param dbName The database name to be deleted.
*/
+ (void)removeDatabase:(NSString *)dbName;
#pragma mark - Sync Messages
/**
* Save sync message to persistent store to check for duplicates.
*
* @param rmqID The rmqID of the message to save.
* @param expirationTime The expiration time of the message to save.
* @param apnsReceived YES if the message was received via APNS else NO.
* @param mcsReceived YES if the message was received via MCS else NO.
* @param error The error if any while saving the message to store.
*
* @return YES if the message was saved successfully else NO.
*/
- (BOOL)saveSyncMessageWithRmqID:(NSString *)rmqID
expirationTime:(int64_t)expirationTime
apnsReceived:(BOOL)apnsReceived
mcsReceived:(BOOL)mcsReceived
error:(NSError **)error;
/**
* Update sync message received via APNS.
*
* @param rmqID The rmqID of the sync message.
* @param error The error if any while updating the sync message in persistence.
*
* @return YES if the update was successful else NO.
*/
- (BOOL)updateSyncMessageViaAPNSWithRmqID:(NSString *)rmqID
error:(NSError **)error;
/**
* Update sync message received via MCS.
*
* @param rmqID The rmqID of the sync message.
* @param error The error if any while updating the sync message in persistence.
*
* @return YES if the update was successful else NO.
*/
- (BOOL)updateSyncMessageViaMCSWithRmqID:(NSString *)rmqID
error:(NSError **)error;
/**
* Query sync message table for a given rmqID.
*
* @param rmqID The rmqID to search for in SYNC_RMQ.
*
* @return The sync message that was persisted with `rmqID`. If no such message was persisted
* return nil.
*/
- (FIRMessagingPersistentSyncMessage *)querySyncMessageWithRmqID:(NSString *)rmqID;
/**
* Delete sync message with rmqID.
*
* @param rmqID The rmqID of the message to delete.
*
* @return YES if a sync message with rmqID was found and deleted successfully else NO.
*/
- (BOOL)deleteSyncMessageWithRmqID:(NSString *)rmqID;
/**
* Delete the expired sync messages from persisten store. Also deletes messages that have been
* delivered both via APNS and MCS.
*
* @param error The error if any while deleting the messages.
*
* @return The total number of messages that were deleted from the persistent store.
*/
- (int)deleteExpiredOrFinishedSyncMessages:(NSError **)error;
@end
@@ -0,0 +1,817 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessagingRmq2PersistentStore.h"
#import <sqlite3.h>
#import "FIRMessagingConstants.h"
#import "FIRMessagingDefines.h"
#import "FIRMessagingLogger.h"
#import "FIRMessagingPersistentSyncMessage.h"
#import "FIRMessagingUtilities.h"
#import "NSError+FIRMessaging.h"
#import "Protos/GtalkCore.pbobjc.h"
#ifndef _FIRMessagingRmqLogAndExit
#define _FIRMessagingRmqLogAndExit(stmt, return_value) \
do { \
[self logErrorAndFinalizeStatement:stmt]; \
return return_value; \
} while(0)
#endif
typedef enum : NSUInteger {
FIRMessagingRmqDirectoryUnknown,
FIRMessagingRmqDirectoryDocuments,
FIRMessagingRmqDirectoryApplicationSupport,
} FIRMessagingRmqDirectory;
static NSString *const kFCMRmqStoreTag = @"FIRMessagingRmqStore:";
// table names
NSString *const kTableOutgoingRmqMessages = @"outgoingRmqMessages";
NSString *const kTableLastRmqId = @"lastrmqid";
NSString *const kOldTableS2DRmqIds = @"s2dRmqIds";
NSString *const kTableS2DRmqIds = @"s2dRmqIds_1";
// Used to prevent de-duping of sync messages received both via APNS and MCS.
NSString *const kTableSyncMessages = @"incomingSyncMessages";
static NSString *const kTablePrefix = @"";
// create tables
static NSString *const kCreateTableOutgoingRmqMessages =
@"create TABLE IF NOT EXISTS %@%@ "
@"(_id INTEGER PRIMARY KEY, "
@"rmq_id INTEGER, "
@"type INTEGER, "
@"ts INTEGER, "
@"data BLOB)";
static NSString *const kCreateTableLastRmqId =
@"create TABLE IF NOT EXISTS %@%@ "
@"(_id INTEGER PRIMARY KEY, "
@"rmq_id INTEGER)";
static NSString *const kCreateTableS2DRmqIds =
@"create TABLE IF NOT EXISTS %@%@ "
@"(_id INTEGER PRIMARY KEY, "
@"rmq_id TEXT)";
static NSString *const kCreateTableSyncMessages =
@"create TABLE IF NOT EXISTS %@%@ "
@"(_id INTEGER PRIMARY KEY, "
@"rmq_id TEXT, "
@"expiration_ts INTEGER, "
@"apns_recv INTEGER, "
@"mcs_recv INTEGER)";
static NSString *const kDropTableCommand =
@"drop TABLE if exists %@%@";
// table infos
static NSString *const kRmqIdColumn = @"rmq_id";
static NSString *const kDataColumn = @"data";
static NSString *const kProtobufTagColumn = @"type";
static NSString *const kIdColumn = @"_id";
static NSString *const kOutgoingRmqMessagesColumns = @"rmq_id, type, data";
// Sync message columns
static NSString *const kSyncMessagesColumns = @"rmq_id, expiration_ts, apns_recv, mcs_recv";
// Message time expiration in seconds since 1970
static NSString *const kSyncMessageExpirationTimestampColumn = @"expiration_ts";
static NSString *const kSyncMessageAPNSReceivedColumn = @"apns_recv";
static NSString *const kSyncMessageMCSReceivedColumn = @"mcs_recv";
// table data handlers
typedef void(^FCMOutgoingRmqMessagesTableHandler)(int64_t rmqId, int8_t tag, NSData *data);
// Utility to create an NSString from a sqlite3 result code
NSString * _Nonnull FIRMessagingStringFromSQLiteResult(int result) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
const char *errorStr = sqlite3_errstr(result);
#pragma pop
NSString *errorString = [NSString stringWithFormat:@"%d - %s", result, errorStr];
return errorString;
}
@interface FIRMessagingRmq2PersistentStore () {
sqlite3 *_database;
}
@property(nonatomic, readwrite, strong) NSString *databaseName;
@property(nonatomic, readwrite, assign) FIRMessagingRmqDirectory currentDirectory;
@end
@implementation FIRMessagingRmq2PersistentStore
- (instancetype)initWithDatabaseName:(NSString *)databaseName {
self = [super init];
if (self) {
_databaseName = [databaseName copy];
#if TARGET_OS_IOS
BOOL didMoveToApplicationSupport =
[self moveToApplicationSupportSubDirectory:kFIRMessagingSubDirectoryName];
_currentDirectory = didMoveToApplicationSupport
? FIRMessagingRmqDirectoryApplicationSupport
: FIRMessagingRmqDirectoryDocuments;
#else
_currentDirectory = FIRMessagingRmqDirectoryApplicationSupport;
#endif
[self openDatabase:_databaseName];
}
return self;
}
- (void)dealloc {
sqlite3_close(_database);
}
- (BOOL)moveToApplicationSupportSubDirectory:(NSString *)subDirectoryName {
NSArray *directoryPaths = NSSearchPathForDirectoriesInDomains(FIRMessagingSupportedDirectory(),
NSUserDomainMask, YES);
NSString *applicationSupportDirPath = directoryPaths.lastObject;
NSArray *components = @[applicationSupportDirPath, subDirectoryName];
NSString *subDirectoryPath = [NSString pathWithComponents:components];
BOOL hasSubDirectory;
if (![[NSFileManager defaultManager] fileExistsAtPath:subDirectoryPath
isDirectory:&hasSubDirectory]) {
// Cannot move to non-existent directory
return NO;
}
if ([self doesFileExistInDirectory:FIRMessagingRmqDirectoryDocuments]) {
NSString *oldPlistPath = [[self class] pathForDatabase:self.databaseName
inDirectory:FIRMessagingRmqDirectoryDocuments];
NSString *newPlistPath = [[self class]
pathForDatabase:self.databaseName
inDirectory:FIRMessagingRmqDirectoryApplicationSupport];
if ([self doesFileExistInDirectory:FIRMessagingRmqDirectoryApplicationSupport]) {
// File exists in both Documents and ApplicationSupport, delete the one in Documents
NSError *deleteError;
if (![[NSFileManager defaultManager] removeItemAtPath:oldPlistPath error:&deleteError]) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore000,
@"Failed to delete old copy of %@.sqlite in Documents %@",
self.databaseName, deleteError);
}
return NO;
}
NSError *moveError;
if (![[NSFileManager defaultManager] moveItemAtPath:oldPlistPath
toPath:newPlistPath
error:&moveError]) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore001,
@"Failed to move file %@ from %@ to %@. Error: %@", self.databaseName,
oldPlistPath, newPlistPath, moveError);
return NO;
}
}
// We moved the file if it existed, otherwise we didn't need to do anything
return YES;
}
- (BOOL)doesFileExistInDirectory:(FIRMessagingRmqDirectory)directory {
NSString *path = [[self class] pathForDatabase:self.databaseName inDirectory:directory];
return [[NSFileManager defaultManager] fileExistsAtPath:path];
}
+ (NSString *)pathForDatabase:(NSString *)dbName inDirectory:(FIRMessagingRmqDirectory)directory {
NSArray *paths;
NSArray *components;
NSString *dbNameWithExtension = [NSString stringWithFormat:@"%@.sqlite", dbName];
NSString *errorMessage;
switch (directory) {
case FIRMessagingRmqDirectoryDocuments:
paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
components = @[paths.lastObject, dbNameWithExtension];
break;
case FIRMessagingRmqDirectoryApplicationSupport:
paths = NSSearchPathForDirectoriesInDomains(FIRMessagingSupportedDirectory(),
NSUserDomainMask,
YES);
components = @[
paths.lastObject,
kFIRMessagingSubDirectoryName,
dbNameWithExtension
];
break;
default:
errorMessage = [NSString stringWithFormat:@"Invalid directory type %lu",
(unsigned long)directory];
FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStoreInvalidRmqDirectory,
@"%@",
errorMessage);
NSAssert(NO, errorMessage);
break;
}
return [NSString pathWithComponents:components];
}
- (void)createTableWithName:(NSString *)tableName command:(NSString *)command {
char *error;
NSString *createDatabase = [NSString stringWithFormat:command, kTablePrefix, tableName];
if (sqlite3_exec(_database, [createDatabase UTF8String], NULL, NULL, &error) != SQLITE_OK) {
// remove db before failing
[self removeDatabase];
NSString *errorMessage = [NSString stringWithFormat:@"Couldn't create table: %@ %@",
kCreateTableOutgoingRmqMessages,
[NSString stringWithCString:error encoding:NSUTF8StringEncoding]];
FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStoreErrorCreatingTable,
@"%@",
errorMessage);
NSAssert(NO, errorMessage);
}
}
- (void)dropTableWithName:(NSString *)tableName {
char *error;
NSString *dropTableSQL = [NSString stringWithFormat:kDropTableCommand, kTablePrefix, tableName];
if (sqlite3_exec(_database, [dropTableSQL UTF8String], NULL, NULL, &error) != SQLITE_OK) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore002,
@"Failed to remove table %@", tableName);
}
}
- (void)removeDatabase {
NSString *path = [[self class] pathForDatabase:self.databaseName
inDirectory:self.currentDirectory];
[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
}
+ (void)removeDatabase:(NSString *)dbName {
NSString *documentsDirPath = [self pathForDatabase:dbName
inDirectory:FIRMessagingRmqDirectoryDocuments];
NSString *standardDirPath =
[self pathForDatabase:dbName inDirectory:FIRMessagingRmqDirectoryApplicationSupport];
[[NSFileManager defaultManager] removeItemAtPath:documentsDirPath error:nil];
[[NSFileManager defaultManager] removeItemAtPath:standardDirPath error:nil];
}
- (void)openDatabase:(NSString *)dbName {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *path = [[self class] pathForDatabase:dbName inDirectory:self.currentDirectory];
BOOL didOpenDatabase = YES;
if (![fileManager fileExistsAtPath:path]) {
// We've to separate between different versions here because of backwards compatbility issues.
int result = sqlite3_open_v2([path UTF8String],
&_database,
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FILEPROTECTION_NONE,
NULL);
if (result != SQLITE_OK) {
NSString *errorString = FIRMessagingStringFromSQLiteResult(result);
NSString *errorMessage =
[NSString stringWithFormat:@"Could not open existing RMQ database at path %@, error: %@",
path,
errorString];
FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStoreErrorOpeningDatabase,
@"%@",
errorMessage);
NSAssert(NO, errorMessage);
return;
}
[self createTableWithName:kTableOutgoingRmqMessages
command:kCreateTableOutgoingRmqMessages];
[self createTableWithName:kTableLastRmqId command:kCreateTableLastRmqId];
[self createTableWithName:kTableS2DRmqIds command:kCreateTableS2DRmqIds];
} else {
// Calling sqlite3_open should create the database, since the file doesn't exist.
int result = sqlite3_open_v2([path UTF8String],
&_database,
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FILEPROTECTION_NONE,
NULL);
if (result != SQLITE_OK) {
NSString *errorString = FIRMessagingStringFromSQLiteResult(result);
NSString *errorMessage =
[NSString stringWithFormat:@"Could not create RMQ database at path %@, error: %@",
path,
errorString];
FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStoreErrorCreatingDatabase,
@"%@",
errorMessage);
NSAssert(NO, errorMessage);
didOpenDatabase = NO;
} else {
[self updateDbWithStringRmqID];
}
}
if (didOpenDatabase) {
[self createTableWithName:kTableSyncMessages command:kCreateTableSyncMessages];
}
}
- (void)updateDbWithStringRmqID {
[self createTableWithName:kTableS2DRmqIds command:kCreateTableS2DRmqIds];
[self dropTableWithName:kOldTableS2DRmqIds];
}
#pragma mark - Insert
- (BOOL)saveUnackedS2dMessageWithRmqId:(NSString *)rmqId {
NSString *insertFormat = @"INSERT INTO %@ (%@) VALUES (?)";
NSString *insertSQL = [NSString stringWithFormat:insertFormat,
kTableS2DRmqIds,
kRmqIdColumn];
sqlite3_stmt *insert_statement;
if (sqlite3_prepare_v2(_database, [insertSQL UTF8String], -1, &insert_statement, NULL)
!= SQLITE_OK) {
_FIRMessagingRmqLogAndExit(insert_statement, NO);
}
if (sqlite3_bind_text(insert_statement,
1,
[rmqId UTF8String],
(int)[rmqId length],
SQLITE_STATIC) != SQLITE_OK) {
_FIRMessagingRmqLogAndExit(insert_statement, NO);
}
if (sqlite3_step(insert_statement) != SQLITE_DONE) {
_FIRMessagingRmqLogAndExit(insert_statement, NO);
}
sqlite3_finalize(insert_statement);
return YES;
}
- (BOOL)saveMessageWithRmqId:(int64_t)rmqId
tag:(int8_t)tag
data:(NSData *)data
error:(NSError **)error {
NSString *insertFormat = @"INSERT INTO %@ (%@, %@, %@) VALUES (?, ?, ?)";
NSString *insertSQL = [NSString stringWithFormat:insertFormat,
kTableOutgoingRmqMessages, // table
kRmqIdColumn, kProtobufTagColumn, kDataColumn /* columns */];
sqlite3_stmt *insert_statement;
if (sqlite3_prepare_v2(_database, [insertSQL UTF8String], -1, &insert_statement, NULL)
!= SQLITE_OK) {
if (error) {
*error = [NSError errorWithDomain:[NSString stringWithFormat:@"%s", sqlite3_errmsg(_database)]
code:sqlite3_errcode(_database)
userInfo:nil];
}
_FIRMessagingRmqLogAndExit(insert_statement, NO);
}
if (sqlite3_bind_int64(insert_statement, 1, rmqId) != SQLITE_OK) {
_FIRMessagingRmqLogAndExit(insert_statement, NO);
}
if (sqlite3_bind_int(insert_statement, 2, tag) != SQLITE_OK) {
_FIRMessagingRmqLogAndExit(insert_statement, NO);
}
if (sqlite3_bind_blob(insert_statement, 3, [data bytes], (int)[data length], NULL) != SQLITE_OK) {
_FIRMessagingRmqLogAndExit(insert_statement, NO);
}
if (sqlite3_step(insert_statement) != SQLITE_DONE) {
_FIRMessagingRmqLogAndExit(insert_statement, NO);
}
sqlite3_finalize(insert_statement);
return YES;
}
- (int)deleteMessagesFromTable:(NSString *)tableName
withRmqIds:(NSArray *)rmqIds {
_FIRMessagingDevAssert([tableName isEqualToString:kTableOutgoingRmqMessages] ||
[tableName isEqualToString:kTableLastRmqId] ||
[tableName isEqualToString:kTableS2DRmqIds] ||
[tableName isEqualToString:kTableSyncMessages],
@"%@: Invalid Table Name %@", kFCMRmqStoreTag, tableName);
BOOL isRmqIDString = NO;
// RmqID is a string only for outgoing messages
if ([tableName isEqualToString:kTableS2DRmqIds] ||
[tableName isEqualToString:kTableSyncMessages]) {
isRmqIDString = YES;
}
NSMutableString *delete = [NSMutableString stringWithFormat:@"DELETE FROM %@ WHERE ", tableName];
NSString *toDeleteArgument = [NSString stringWithFormat:@"%@ = ? OR ", kRmqIdColumn];
int toDelete = (int)[rmqIds count];
if (toDelete == 0) {
return 0;
}
int maxBatchSize = 100;
int start = 0;
int deleteCount = 0;
while (start < toDelete) {
// construct the WHERE argument
int end = MIN(start + maxBatchSize, toDelete);
NSMutableString *whereArgument = [NSMutableString string];
for (int i = start; i < end; i++) {
[whereArgument appendString:toDeleteArgument];
}
// remove the last * OR * from argument
NSRange range = NSMakeRange([whereArgument length] -4, 4);
[whereArgument deleteCharactersInRange:range];
NSString *deleteQuery = [NSString stringWithFormat:@"%@ %@", delete, whereArgument];
// sqlite update
sqlite3_stmt *delete_statement;
if (sqlite3_prepare_v2(_database, [deleteQuery UTF8String],
-1, &delete_statement, NULL) != SQLITE_OK) {
_FIRMessagingRmqLogAndExit(delete_statement, 0);
}
// bind values
int rmqIndex = 0;
int placeholderIndex = 1; // placeholders in sqlite3 start with 1
for (NSString *rmqId in rmqIds) { // objectAtIndex: is O(n) -- would make it slow
if (rmqIndex < start) {
rmqIndex++;
continue;
} else if (rmqIndex >= end) {
break;
} else {
if (isRmqIDString) {
if (sqlite3_bind_text(delete_statement,
placeholderIndex,
[rmqId UTF8String],
(int)[rmqId length],
SQLITE_STATIC) != SQLITE_OK) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRmq2PersistentStore003,
@"Failed to bind rmqID %@", rmqId);
continue;
}
} else {
int64_t rmqIdValue = [rmqId longLongValue];
sqlite3_bind_int64(delete_statement, placeholderIndex, rmqIdValue);
}
placeholderIndex++;
}
rmqIndex++;
}
if (sqlite3_step(delete_statement) != SQLITE_DONE) {
_FIRMessagingRmqLogAndExit(delete_statement, deleteCount);
}
sqlite3_finalize(delete_statement);
deleteCount += sqlite3_changes(_database);
start = end;
}
// if we are here all of our sqlite queries should have succeeded
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRmq2PersistentStore004,
@"%@ Trying to delete %d s2D ID's, successfully deleted %d",
kFCMRmqStoreTag, toDelete, deleteCount);
return deleteCount;
}
#pragma mark - Query
- (int64_t)queryHighestRmqId {
NSString *queryFormat = @"SELECT %@ FROM %@ ORDER BY %@ DESC LIMIT %d";
NSString *query = [NSString stringWithFormat:queryFormat,
kRmqIdColumn, // column
kTableOutgoingRmqMessages, // table
kRmqIdColumn, // order by column
1]; // limit
sqlite3_stmt *statement;
int64_t highestRmqId = 0;
if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
_FIRMessagingRmqLogAndExit(statement, highestRmqId);
}
if (sqlite3_step(statement) == SQLITE_ROW) {
highestRmqId = sqlite3_column_int64(statement, 0);
}
sqlite3_finalize(statement);
return highestRmqId;
}
- (int64_t)queryLastRmqId {
NSString *queryFormat = @"SELECT %@ FROM %@ ORDER BY %@ DESC LIMIT %d";
NSString *query = [NSString stringWithFormat:queryFormat,
kRmqIdColumn, // column
kTableLastRmqId, // table
kRmqIdColumn, // order by column
1]; // limit
sqlite3_stmt *statement;
int64_t lastRmqId = 0;
if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
_FIRMessagingRmqLogAndExit(statement, lastRmqId);
}
if (sqlite3_step(statement) == SQLITE_ROW) {
lastRmqId = sqlite3_column_int64(statement, 0);
}
sqlite3_finalize(statement);
return lastRmqId;
}
- (BOOL)updateLastOutgoingRmqId:(int64_t)rmqID {
NSString *queryFormat = @"INSERT OR REPLACE INTO %@ (%@, %@) VALUES (?, ?)";
NSString *query = [NSString stringWithFormat:queryFormat,
kTableLastRmqId, // table
kIdColumn, kRmqIdColumn]; // columns
sqlite3_stmt *statement;
if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
_FIRMessagingRmqLogAndExit(statement, NO);
}
if (sqlite3_bind_int(statement, 1, 1) != SQLITE_OK) {
_FIRMessagingRmqLogAndExit(statement, NO);
}
if (sqlite3_bind_int64(statement, 2, rmqID) != SQLITE_OK) {
_FIRMessagingRmqLogAndExit(statement, NO);
}
if (sqlite3_step(statement) != SQLITE_DONE) {
_FIRMessagingRmqLogAndExit(statement, NO);
}
sqlite3_finalize(statement);
return YES;
}
- (NSArray *)unackedS2dRmqIds {
NSString *queryFormat = @"SELECT %@ FROM %@ ORDER BY %@ ASC";
NSString *query = [NSString stringWithFormat:queryFormat,
kRmqIdColumn,
kTableS2DRmqIds,
kRmqIdColumn];
sqlite3_stmt *statement;
if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRmq2PersistentStore005,
@"%@: Could not find s2d ids", kFCMRmqStoreTag);
_FIRMessagingRmqLogAndExit(statement, @[]);
}
NSMutableArray *rmqIDArray = [NSMutableArray array];
while (sqlite3_step(statement) == SQLITE_ROW) {
const char *rmqID = (char *)sqlite3_column_text(statement, 0);
[rmqIDArray addObject:[NSString stringWithUTF8String:rmqID]];
}
sqlite3_finalize(statement);
return rmqIDArray;
}
#pragma mark - Scan
- (void)scanOutgoingRmqMessagesWithHandler:(FCMOutgoingRmqMessagesTableHandler)handler {
static NSString *queryFormat = @"SELECT %@ FROM %@ WHERE %@ != 0 ORDER BY %@ ASC";
NSString *query = [NSString stringWithFormat:queryFormat,
kOutgoingRmqMessagesColumns, // select (rmq_id, type, data)
kTableOutgoingRmqMessages, // from table
kRmqIdColumn, // where
kRmqIdColumn]; // order by
sqlite3_stmt *statement;
if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
[self logError];
sqlite3_finalize(statement);
return;
}
// can query sqlite3 for this but this is fine
const int rmqIdColumnNumber = 0;
const int typeColumnNumber = 1;
const int dataColumnNumber = 2;
while (sqlite3_step(statement) == SQLITE_ROW) {
int64_t rmqId = sqlite3_column_int64(statement, rmqIdColumnNumber);
int8_t type = sqlite3_column_int(statement, typeColumnNumber);
const void *bytes = sqlite3_column_blob(statement, dataColumnNumber);
int length = sqlite3_column_bytes(statement, dataColumnNumber);
_FIRMessagingDevAssert(bytes != NULL,
@"%@ Message with no data being stored in Rmq",
kFCMRmqStoreTag);
NSData *data = [NSData dataWithBytes:bytes length:length];
handler(rmqId, type, data);
}
sqlite3_finalize(statement);
}
#pragma mark - Sync Messages
- (FIRMessagingPersistentSyncMessage *)querySyncMessageWithRmqID:(NSString *)rmqID {
_FIRMessagingDevAssert([rmqID length], @"Invalid rmqID key %@ to search in SYNC_RMQ", rmqID);
NSString *queryFormat = @"SELECT %@ FROM %@ WHERE %@ = '%@'";
NSString *query = [NSString stringWithFormat:queryFormat,
kSyncMessagesColumns, // SELECT (rmq_id, expiration_ts, apns_recv, mcs_recv)
kTableSyncMessages, // FROM sync_rmq
kRmqIdColumn, // WHERE rmq_id
rmqID];
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
[self logError];
sqlite3_finalize(stmt);
return nil;
}
const int rmqIDColumn = 0;
const int expirationTimestampColumn = 1;
const int apnsReceivedColumn = 2;
const int mcsReceivedColumn = 3;
int count = 0;
FIRMessagingPersistentSyncMessage *persistentMessage;
while (sqlite3_step(stmt) == SQLITE_ROW) {
NSString *rmqID =
[NSString stringWithUTF8String:(char *)sqlite3_column_text(stmt, rmqIDColumn)];
int64_t expirationTimestamp = sqlite3_column_int64(stmt, expirationTimestampColumn);
BOOL apnsReceived = sqlite3_column_int(stmt, apnsReceivedColumn);
BOOL mcsReceived = sqlite3_column_int(stmt, mcsReceivedColumn);
// create a new persistent message
persistentMessage =
[[FIRMessagingPersistentSyncMessage alloc] initWithRMQID:rmqID expirationTime:expirationTimestamp];
persistentMessage.apnsReceived = apnsReceived;
persistentMessage.mcsReceived = mcsReceived;
count++;
}
sqlite3_finalize(stmt);
_FIRMessagingDevAssert(count <= 1, @"Found multiple messages in %@ with same RMQ ID", kTableSyncMessages);
return persistentMessage;
}
- (BOOL)deleteSyncMessageWithRmqID:(NSString *)rmqID {
_FIRMessagingDevAssert([rmqID length], @"Invalid rmqID key %@ to delete in SYNC_RMQ", rmqID);
return [self deleteMessagesFromTable:kTableSyncMessages withRmqIds:@[rmqID]] > 0;
}
- (int)deleteExpiredOrFinishedSyncMessages:(NSError *__autoreleasing *)error {
int64_t now = FIRMessagingCurrentTimestampInSeconds();
NSString *deleteSQL = @"DELETE FROM %@ "
@"WHERE %@ < %lld OR " // expirationTime < now
@"(%@ = 1 AND %@ = 1)"; // apns_received = 1 AND mcs_received = 1
NSString *query = [NSString stringWithFormat:deleteSQL,
kTableSyncMessages,
kSyncMessageExpirationTimestampColumn,
now,
kSyncMessageAPNSReceivedColumn,
kSyncMessageMCSReceivedColumn];
NSString *errorReason = @"Failed to save delete expired sync messages from store.";
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
if (error) {
*error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
userInfo:@{ @"error" : errorReason }];
}
_FIRMessagingRmqLogAndExit(stmt, 0);
}
if (sqlite3_step(stmt) != SQLITE_DONE) {
if (error) {
*error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
userInfo:@{ @"error" : errorReason }];
}
_FIRMessagingRmqLogAndExit(stmt, 0);
}
sqlite3_finalize(stmt);
int deleteCount = sqlite3_changes(_database);
return deleteCount;
}
- (BOOL)saveSyncMessageWithRmqID:(NSString *)rmqID
expirationTime:(int64_t)expirationTime
apnsReceived:(BOOL)apnsReceived
mcsReceived:(BOOL)mcsReceived
error:(NSError **)error {
_FIRMessagingDevAssert([rmqID length], @"Invalid nil message to persist to SYNC_RMQ");
NSString *insertFormat = @"INSERT INTO %@ (%@, %@, %@, %@) VALUES (?, ?, ?, ?)";
NSString *insertSQL = [NSString stringWithFormat:insertFormat,
kTableSyncMessages, // Table name
kRmqIdColumn, // rmq_id
kSyncMessageExpirationTimestampColumn, // expiration_ts
kSyncMessageAPNSReceivedColumn, // apns_recv
kSyncMessageMCSReceivedColumn /* mcs_recv */];
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(_database, [insertSQL UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
if (error) {
*error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
userInfo:@{ @"error" : @"Failed to save sync message to store." }];
}
_FIRMessagingRmqLogAndExit(stmt, NO);
}
if (sqlite3_bind_text(stmt, 1, [rmqID UTF8String], (int)[rmqID length], NULL) != SQLITE_OK) {
_FIRMessagingRmqLogAndExit(stmt, NO);
}
if (sqlite3_bind_int64(stmt, 2, expirationTime) != SQLITE_OK) {
_FIRMessagingRmqLogAndExit(stmt, NO);
}
if (sqlite3_bind_int(stmt, 3, apnsReceived ? 1 : 0) != SQLITE_OK) {
_FIRMessagingRmqLogAndExit(stmt, NO);
}
if (sqlite3_bind_int(stmt, 4, mcsReceived ? 1 : 0) != SQLITE_OK) {
_FIRMessagingRmqLogAndExit(stmt, NO);
}
if (sqlite3_step(stmt) != SQLITE_DONE) {
_FIRMessagingRmqLogAndExit(stmt, NO);
}
sqlite3_finalize(stmt);
return YES;
}
- (BOOL)updateSyncMessageViaAPNSWithRmqID:(NSString *)rmqID
error:(NSError **)error {
return [self updateSyncMessageWithRmqID:rmqID
column:kSyncMessageAPNSReceivedColumn
value:YES
error:error];
}
- (BOOL)updateSyncMessageViaMCSWithRmqID:(NSString *)rmqID
error:(NSError *__autoreleasing *)error {
return [self updateSyncMessageWithRmqID:rmqID
column:kSyncMessageMCSReceivedColumn
value:YES
error:error];
}
- (BOOL)updateSyncMessageWithRmqID:(NSString *)rmqID
column:(NSString *)column
value:(BOOL)value
error:(NSError **)error {
_FIRMessagingDevAssert([column isEqualToString:kSyncMessageAPNSReceivedColumn] ||
[column isEqualToString:kSyncMessageMCSReceivedColumn],
@"Invalid column name %@ for SYNC_RMQ", column);
NSString *queryFormat = @"UPDATE %@ " // Table name
@"SET %@ = %d " // column=value
@"WHERE %@ = ?"; // condition
NSString *query = [NSString stringWithFormat:queryFormat,
kTableSyncMessages,
column,
value ? 1 : 0,
kRmqIdColumn];
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
if (error) {
*error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
userInfo:@{ @"error" : @"Failed to update sync message"}];
}
_FIRMessagingRmqLogAndExit(stmt, NO);
}
if (sqlite3_bind_text(stmt, 1, [rmqID UTF8String], (int)[rmqID length], NULL) != SQLITE_OK) {
_FIRMessagingRmqLogAndExit(stmt, NO);
}
if (sqlite3_step(stmt) != SQLITE_DONE) {
_FIRMessagingRmqLogAndExit(stmt, NO);
}
sqlite3_finalize(stmt);
return YES;
}
#pragma mark - Private
- (NSString *)lastErrorMessage {
return [NSString stringWithFormat:@"%s", sqlite3_errmsg(_database)];
}
- (int)lastErrorCode {
return sqlite3_errcode(_database);
}
- (void)logError {
FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore006,
@"%@ error: code (%d) message: %@", kFCMRmqStoreTag, [self lastErrorCode],
[self lastErrorMessage]);
}
- (void)logErrorAndFinalizeStatement:(sqlite3_stmt *)stmt {
[self logError];
sqlite3_finalize(stmt);
}
@end
@@ -0,0 +1,190 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
@class GtalkDataMessageStanza;
@class GPBMessage;
@class FIRMessagingPersistentSyncMessage;
/**
* Called on each raw message.
*/
typedef void(^FIRMessagingRmqMessageHandler)(int64_t rmqId, int8_t tag, NSData *data);
/**
* Called on each DataMessageStanza.
*/
typedef void(^FIRMessagingDataMessageHandler)(int64_t rmqId, GtalkDataMessageStanza *stanza);
/**
* Used to scan through the rmq and perform actions on messages as required.
*/
@protocol FIRMessagingRmqScanner <NSObject>
/**
* Scan the RMQ for outgoing messages and process them as required.
*/
- (void)scanWithRmqMessageHandler:(FIRMessagingRmqMessageHandler)rmqMessageHandler
dataMessageHandler:(FIRMessagingDataMessageHandler)dataMessageHandler;
@end
/**
* This manages the RMQ persistent store.
*
* The store is used to store all the S2D id's that were received by the client and were ACK'ed
* by us but the server hasn't confirmed the ACK. We don't delete these id's until the server
* ACK's us that they have received them.
*
* We also store the upstream messages(d2s) that were sent by the client.
*
* Also store the lastRMQId that was sent by us so that for a new connection being setup we don't
* duplicate RMQ Id's for the new messages.
*/
@interface FIRMessagingRmqManager : NSObject <FIRMessagingRmqScanner>
// designated initializer
- (instancetype)initWithDatabaseName:(NSString *)databaseName;
- (void)loadRmqId;
/**
* Save an upstream message to RMQ. If the message send fails for some reason we would not
* lose the message since it would be saved in the RMQ.
*
* @param message The upstream message to be saved.
* @param error The error if any while saving the message else nil.
*
* @return YES if the message was successfully saved to RMQ else NO.
*/
- (BOOL)saveRmqMessage:(GPBMessage *)message error:(NSError **)error;
/**
* Save Server to device message with the given RMQ-ID.
*
* @param rmqID The rmqID of the s2d message to save.
*
* @return YES if the save was successfull else NO.
*/
- (BOOL)saveS2dMessageWithRmqId:(NSString *)rmqID;
/**
* A list of all unacked Server to device RMQ IDs.
*
* @return A list of unacked Server to Device RMQ ID's. All values are Strings.
*/
- (NSArray *)unackedS2dRmqIds;
/**
* Removes the outgoing message from RMQ store.
*
* @param rmqId The rmqID to remove from the store.
*
* @return The number of messages deleted successfully.
*/
- (int)removeRmqMessagesWithRmqId:(NSString *)rmqId;
/**
* Removes the messages with the given rmqIDs from RMQ store.
*
* @param rmqIds The lsit of rmqID's to remove from the store.
*
* @return The number of messages deleted successfully.
*/
- (int)removeRmqMessagesWithRmqIds:(NSArray *)rmqIds;
/**
* Removes a list of downstream messages from the RMQ.
*
* @param s2dIds The list of messages ACK'ed by the server that we should remove
* from the RMQ store.
*/
- (void)removeS2dIds:(NSArray *)s2dIds;
#pragma mark - Sync Messages
/**
* Get persisted sync message with rmqID.
*
* @param rmqID The rmqID of the persisted sync message.
*
* @return A valid persistent sync message with the given rmqID if found in the RMQ else nil.
*/
- (FIRMessagingPersistentSyncMessage *)querySyncMessageWithRmqID:(NSString *)rmqID;
/**
* Delete sync message with rmqID.
*
* @param rmqID The rmqID of the persisted sync message.
*
* @return YES if the message was successfully deleted else NO.
*/
- (BOOL)deleteSyncMessageWithRmqID:(NSString *)rmqID;
/**
* Delete the expired sync messages from persisten store. Also deletes messages that have been
* delivered both via APNS and MCS.
*
* @param error The error if any while deleting the messages.
*
* @return The total number of messages that were deleted from the persistent store.
*/
- (int)deleteExpiredOrFinishedSyncMessages:(NSError **)error;
/**
* Save sync message received by the device.
*
* @param rmqID The rmqID of the message received.
* @param expirationTime The expiration time of the sync message received.
* @param apnsReceived YES if the message was received via APNS else NO.
* @param mcsReceived YES if the message was received via MCS else NO.
* @param error The error if any while saving the sync message to persistent store.
*
* @return YES if the message save was successful else NO.
*/
- (BOOL)saveSyncMessageWithRmqID:(NSString *)rmqID
expirationTime:(int64_t)expirationTime
apnsReceived:(BOOL)apnsReceived
mcsReceived:(BOOL)mcsReceived
error:(NSError **)error;
/**
* Update sync message received via APNS.
*
* @param rmqID The rmqID of the received message.
* @param error The error if any while updating the sync message.
*
* @return YES if the persistent sync message was successfully updated else NO.
*/
- (BOOL)updateSyncMessageViaAPNSWithRmqID:(NSString *)rmqID error:(NSError **)error;
/**
* Update sync message received via MCS.
*
* @param rmqID The rmqID of the received message.
* @param error The error if any while updating the sync message.
*
* @return YES if the persistent sync message was successfully updated else NO.
*/
- (BOOL)updateSyncMessageViaMCSWithRmqID:(NSString *)rmqID error:(NSError **)error;
#pragma mark - Testing
+ (void)removeDatabaseWithName:(NSString *)dbName;
@end
@@ -0,0 +1,264 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessagingRmqManager.h"
#import <sqlite3.h>
#import "FIRMessagingDefines.h"
#import "FIRMessagingLogger.h"
#import "FIRMessagingRmq2PersistentStore.h"
#import "FIRMessagingUtilities.h"
#import "Protos/GtalkCore.pbobjc.h"
#ifndef _FIRMessagingRmqLogAndExit
#define _FIRMessagingRmqLogAndExit(stmt, return_value) \
do { \
[self logErrorAndFinalizeStatement:stmt]; \
return return_value; \
} while(0)
#endif
static NSString *const kFCMRmqTag = @"FIRMessagingRmq:";
@interface FIRMessagingRmqManager ()
@property(nonatomic, readwrite, strong) FIRMessagingRmq2PersistentStore *rmq2Store;
// map the category of an outgoing message with the number of messages for that category
// should always have two keys -- the app, gcm
@property(nonatomic, readwrite, strong) NSMutableDictionary *outstandingMessages;
// Outgoing RMQ persistent id
@property(nonatomic, readwrite, assign) int64_t rmqId;
@end
@implementation FIRMessagingRmqManager
- (instancetype)initWithDatabaseName:(NSString *)databaseName {
self = [super init];
if (self) {
_FIRMessagingDevAssert([databaseName length] > 0, @"RMQ: Invalid rmq db name");
_rmq2Store = [[FIRMessagingRmq2PersistentStore alloc] initWithDatabaseName:databaseName];
_outstandingMessages = [NSMutableDictionary dictionaryWithCapacity:2];
_rmqId = -1;
}
return self;
}
- (void)loadRmqId {
if (self.rmqId >= 0) {
return; // already done
}
[self loadInitialOutgoingPersistentId];
if (self.outstandingMessages.count) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRmqManager000,
@"%@: outstanding categories %ld", kFCMRmqTag,
_FIRMessaging_UL(self.outstandingMessages.count));
}
}
/**
* Initialize the 'initial RMQ':
* - max ID of any message in the queue
* - if the queue is empty, stored value in separate DB.
*
* Stream acks will remove from RMQ, when we remove the highest message we keep track
* of its ID.
*/
- (void)loadInitialOutgoingPersistentId {
// we shouldn't always trust the lastRmqId stored in the LastRmqId table, because
// we only save to the LastRmqId table once in a while (after getting the lastRmqId sent
// by the server after reconnect, and after getting a rmq ack from the server). The
// rmq message with the highest rmq id tells the real story, so check against that first.
int64_t rmqId = [self queryHighestRmqId];
if (rmqId == 0) {
rmqId = [self querylastRmqId];
}
self.rmqId = rmqId + 1;
}
#pragma mark - Save
/**
* Save a message to RMQ2. Will populate the rmq2 persistent ID.
*/
- (BOOL)saveRmqMessage:(GPBMessage *)message
error:(NSError **)error {
// send using rmq2manager
// the wire format of rmq2 id is a string. However, we keep it as a long internally
// in the database. So only convert the id to string when preparing for sending over
// the wire.
NSString *rmq2Id = FIRMessagingGetRmq2Id(message);
if (![rmq2Id length]) {
int64_t rmqId = [self nextRmqId];
rmq2Id = [NSString stringWithFormat:@"%lld", rmqId];
FIRMessagingSetRmq2Id(message, rmq2Id);
}
FIRMessagingProtoTag tag = FIRMessagingGetTagForProto(message);
return [self saveMessage:message withRmqId:[rmq2Id integerValue] tag:tag error:error];
}
- (BOOL)saveMessage:(GPBMessage *)message
withRmqId:(int64_t)rmqId
tag:(int8_t)tag
error:(NSError **)error {
NSData *data = [message data];
return [self.rmq2Store saveMessageWithRmqId:rmqId tag:tag data:data error:error];
}
/**
* This is called when we delete the largest outgoing message from queue.
*/
- (void)saveLastOutgoingRmqId:(int64_t)rmqID {
[self.rmq2Store updateLastOutgoingRmqId:rmqID];
}
- (BOOL)saveS2dMessageWithRmqId:(NSString *)rmqID {
return [self.rmq2Store saveUnackedS2dMessageWithRmqId:rmqID];
}
#pragma mark - Query
- (int64_t)queryHighestRmqId {
return [self.rmq2Store queryHighestRmqId];
}
- (int64_t)querylastRmqId {
return [self.rmq2Store queryLastRmqId];
}
- (NSArray *)unackedS2dRmqIds {
return [self.rmq2Store unackedS2dRmqIds];
}
#pragma mark - FIRMessagingRMQScanner protocol
/**
* We don't have a 'getMessages' method - it would require loading in memory
* the entire content body of all messages.
*
* Instead we iterate and call 'resend' for each message.
*
* This is called:
* - on connect MCS, to resend any outstanding messages
* - init
*/
- (void)scanWithRmqMessageHandler:(FIRMessagingRmqMessageHandler)rmqMessageHandler
dataMessageHandler:(FIRMessagingDataMessageHandler)dataMessageHandler {
// no need to scan database with no callbacks
if (rmqMessageHandler || dataMessageHandler) {
[self.rmq2Store scanOutgoingRmqMessagesWithHandler:^(int64_t rmqId, int8_t tag, NSData *data) {
if (rmqMessageHandler != nil) {
rmqMessageHandler(rmqId, tag, data);
}
if (dataMessageHandler != nil && kFIRMessagingProtoTagDataMessageStanza == tag) {
GPBMessage *proto =
[FIRMessagingGetClassForTag((FIRMessagingProtoTag)tag) parseFromData:data error:NULL];
GtalkDataMessageStanza *stanza = (GtalkDataMessageStanza *)proto;
dataMessageHandler(rmqId, stanza);
}
}];
}
}
#pragma mark - Remove
- (void)ackReceivedForRmqId:(NSString *)rmqId {
// TODO: Optional book-keeping
}
- (int)removeRmqMessagesWithRmqId:(NSString *)rmqId {
return [self removeRmqMessagesWithRmqIds:@[rmqId]];
}
- (int)removeRmqMessagesWithRmqIds:(NSArray *)rmqIds {
if (![rmqIds count]) {
return 0;
}
for (NSString *rmqId in rmqIds) {
[self ackReceivedForRmqId:rmqId];
}
int64_t maxRmqId = -1;
for (NSString *rmqId in rmqIds) {
int64_t rmqIdValue = [rmqId longLongValue];
if (rmqIdValue > maxRmqId) {
maxRmqId = rmqIdValue;
}
}
maxRmqId++;
if (maxRmqId >= self.rmqId) {
[self saveLastOutgoingRmqId:maxRmqId];
}
return [self.rmq2Store deleteMessagesFromTable:kTableOutgoingRmqMessages withRmqIds:rmqIds];
}
- (void)removeS2dIds:(NSArray *)s2dIds {
[self.rmq2Store deleteMessagesFromTable:kTableS2DRmqIds withRmqIds:s2dIds];
}
#pragma mark - Sync Messages
// TODO: RMQManager should also have a cache for all the sync messages
// so we don't hit the DB each time.
- (FIRMessagingPersistentSyncMessage *)querySyncMessageWithRmqID:(NSString *)rmqID {
return [self.rmq2Store querySyncMessageWithRmqID:rmqID];
}
- (BOOL)deleteSyncMessageWithRmqID:(NSString *)rmqID {
return [self.rmq2Store deleteSyncMessageWithRmqID:rmqID];
}
- (int)deleteExpiredOrFinishedSyncMessages:(NSError **)error {
return [self.rmq2Store deleteExpiredOrFinishedSyncMessages:error];
}
- (BOOL)saveSyncMessageWithRmqID:(NSString *)rmqID
expirationTime:(int64_t)expirationTime
apnsReceived:(BOOL)apnsReceived
mcsReceived:(BOOL)mcsReceived
error:(NSError *__autoreleasing *)error {
return [self.rmq2Store saveSyncMessageWithRmqID:rmqID
expirationTime:expirationTime
apnsReceived:apnsReceived
mcsReceived:mcsReceived
error:error];
}
- (BOOL)updateSyncMessageViaAPNSWithRmqID:(NSString *)rmqID error:(NSError **)error {
return [self.rmq2Store updateSyncMessageViaAPNSWithRmqID:rmqID error:error];
}
- (BOOL)updateSyncMessageViaMCSWithRmqID:(NSString *)rmqID error:(NSError **)error {
return [self.rmq2Store updateSyncMessageViaMCSWithRmqID:rmqID error:error];
}
#pragma mark - Testing
+ (void)removeDatabaseWithName:(NSString *)dbName {
[FIRMessagingRmq2PersistentStore removeDatabase:dbName];
}
#pragma mark - Private
- (int64_t)nextRmqId {
return ++self.rmqId;
}
@end
@@ -0,0 +1,56 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
typedef NS_ENUM(NSUInteger, FIRMessagingSecureSocketState){
kFIRMessagingSecureSocketNotOpen = 0,
kFIRMessagingSecureSocketOpening,
kFIRMessagingSecureSocketOpen,
kFIRMessagingSecureSocketClosing,
kFIRMessagingSecureSocketClosed,
kFIRMessagingSecureSocketError
};
@class FIRMessagingSecureSocket;
@protocol FIRMessagingSecureSocketDelegate<NSObject>
- (void)secureSocket:(FIRMessagingSecureSocket *)socket
didReceiveData:(NSData *)data
withTag:(int8_t)tag;
- (void)secureSocket:(FIRMessagingSecureSocket *)socket
didSendProtoWithTag:(int8_t)tag
rmqId:(NSString *)rmqId;
- (void)secureSocketDidConnect:(FIRMessagingSecureSocket *)socket;
- (void)didDisconnectWithSecureSocket:(FIRMessagingSecureSocket *)socket;
@end
/**
* This manages the input/output streams connected to the MCS server. Used to receive data from
* the server and send to it over the wire.
*/
@interface FIRMessagingSecureSocket : NSObject
@property(nonatomic, readwrite, weak) id<FIRMessagingSecureSocketDelegate> delegate;
@property(nonatomic, readonly, assign) FIRMessagingSecureSocketState state;
- (void)connectToHost:(NSString *)host port:(NSUInteger)port onRunLoop:(NSRunLoop *)runLoop;
- (void)disconnect;
- (void)sendData:(NSData *)data withTag:(int8_t)tag rmqId:(NSString *)rmqId;
@end
@@ -0,0 +1,444 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessagingSecureSocket.h"
#import <Protobuf/GPBMessage.h>
#import <Protobuf/GPBCodedOutputStream.h>
#import <Protobuf/GPBUtilities.h>
#import "FIRMessagingCodedInputStream.h"
#import "FIRMessagingDefines.h"
#import "FIRMessagingLogger.h"
#import "FIRMessagingPacketQueue.h"
static const NSUInteger kMaxBufferLength = 1024 * 1024; // 1M
static const NSUInteger kBufferLengthIncrement = 16 * 1024; // 16k
static const uint8_t kVersion = 40;
static const uint8_t kInvalidTag = -1;
typedef NS_ENUM(NSUInteger, FIRMessagingSecureSocketReadResult) {
kFIRMessagingSecureSocketReadResultNone,
kFIRMessagingSecureSocketReadResultIncomplete,
kFIRMessagingSecureSocketReadResultCorrupt,
kFIRMessagingSecureSocketReadResultSuccess
};
static int32_t LogicalRightShift32(int32_t value, int32_t spaces) {
return (int32_t)((uint32_t)(value) >> spaces);
}
static NSUInteger SerializedSize(int32_t value) {
NSUInteger bytes = 0;
while (YES) {
if ((value & ~0x7F) == 0) {
bytes += sizeof(uint8_t);
return bytes;
} else {
bytes += sizeof(uint8_t);
value = LogicalRightShift32(value, 7);
}
}
}
@interface FIRMessagingSecureSocket() <NSStreamDelegate>
@property(nonatomic, readwrite, assign) FIRMessagingSecureSocketState state;
@property(nonatomic, readwrite, strong) NSInputStream *inStream;
@property(nonatomic, readwrite, strong) NSOutputStream *outStream;
@property(nonatomic, readwrite, strong) NSMutableData *inputBuffer;
@property(nonatomic, readwrite, assign) NSUInteger inputBufferLength;
@property(nonatomic, readwrite, strong) NSMutableData *outputBuffer;
@property(nonatomic, readwrite, assign) NSUInteger outputBufferLength;
@property(nonatomic, readwrite, strong) FIRMessagingPacketQueue *packetQueue;
@property(nonatomic, readwrite, assign) BOOL isVersionSent;
@property(nonatomic, readwrite, assign) BOOL isVersionReceived;
@property(nonatomic, readwrite, assign) BOOL isInStreamOpen;
@property(nonatomic, readwrite, assign) BOOL isOutStreamOpen;
@property(nonatomic, readwrite, strong) NSRunLoop *runLoop;
@property(nonatomic, readwrite, strong) NSString *currentRmqIdBeingSent;
@property(nonatomic, readwrite, assign) int8_t currentProtoTypeBeingSent;
@end
@implementation FIRMessagingSecureSocket
- (instancetype)init {
self = [super init];
if (self) {
_state = kFIRMessagingSecureSocketNotOpen;
_inputBuffer = [NSMutableData dataWithLength:kBufferLengthIncrement];
_packetQueue = [[FIRMessagingPacketQueue alloc] init];
_currentProtoTypeBeingSent = kInvalidTag;
}
return self;
}
- (void)dealloc {
[self disconnect];
}
- (void)connectToHost:(NSString *)host
port:(NSUInteger)port
onRunLoop:(NSRunLoop *)runLoop {
_FIRMessagingDevAssert(host != nil, @"Invalid host");
_FIRMessagingDevAssert(runLoop != nil, @"Invalid runloop");
_FIRMessagingDevAssert(self.state == kFIRMessagingSecureSocketNotOpen, @"Socket is already connected");
if (!host || self.state != kFIRMessagingSecureSocketNotOpen) {
return;
}
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket000,
@"Opening secure socket to FIRMessaging service");
self.state = kFIRMessagingSecureSocketOpening;
self.runLoop = runLoop;
CFReadStreamRef inputStreamRef;
CFWriteStreamRef outputStreamRef;
CFStreamCreatePairWithSocketToHost(NULL,
(__bridge CFStringRef)host,
(int)port,
&inputStreamRef,
&outputStreamRef);
self.inStream = CFBridgingRelease(inputStreamRef);
self.outStream = CFBridgingRelease(outputStreamRef);
if (!self.inStream || !self.outStream) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket001,
@"Failed to initialize socket.");
return;
}
self.isInStreamOpen = NO;
self.isOutStreamOpen = NO;
BOOL isVOIPSocket = NO;
[self openStream:self.outStream isVOIPStream:isVOIPSocket];
[self openStream:self.inStream isVOIPStream:isVOIPSocket];
}
- (void)disconnect {
if (self.state == kFIRMessagingSecureSocketClosing) {
return;
}
if (!self.inStream && !self.outStream) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket002,
@"The socket is not open or already closed.");
_FIRMessagingDevAssert(self.state == kFIRMessagingSecureSocketClosed || self.state == kFIRMessagingSecureSocketNotOpen,
@"Socket is already disconnected.");
return;
}
self.state = kFIRMessagingSecureSocketClosing;
if (self.inStream) {
[self closeStream:self.inStream];
self.inStream = nil;
}
if (self.outStream) {
[self closeStream:self.outStream];
self.outStream = nil;
}
self.state = kFIRMessagingSecureSocketClosed;
[self.delegate didDisconnectWithSecureSocket:self];
}
- (void)sendData:(NSData *)data withTag:(int8_t)tag rmqId:(NSString *)rmqId {
[self.packetQueue push:[FIRMessagingPacket packetWithTag:tag rmqId:rmqId data:data]];
if ([self.outStream hasSpaceAvailable]) {
[self performWrite];
}
}
#pragma mark - NSStreamDelegate
- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {
switch (eventCode) {
case NSStreamEventHasBytesAvailable:
if (self.state != kFIRMessagingSecureSocketOpen) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket003,
@"Try to read from socket that is not opened");
return;
}
_FIRMessagingDevAssert(stream == self.inStream, @"Incorrect stream");
if (![self performRead]) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket004,
@"Error occurred when reading incoming stream");
[self disconnect];
}
break;
case NSStreamEventEndEncountered:
FIRMessagingLoggerDebug(
kFIRMessagingMessageCodeSecureSocket005, @"%@ end encountered",
stream == self.inStream
? @"Input stream"
: (stream == self.outStream ? @"Output stream" : @"Unknown stream"));
[self disconnect];
break;
case NSStreamEventOpenCompleted:
if (stream == self.inStream) {
self.isInStreamOpen = YES;
} else if (stream == self.outStream) {
self.isOutStreamOpen = YES;
}
if (self.isInStreamOpen && self.isOutStreamOpen) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket006,
@"Secure socket to FIRMessaging service opened");
self.state = kFIRMessagingSecureSocketOpen;
[self.delegate secureSocketDidConnect:self];
}
break;
case NSStreamEventErrorOccurred: {
FIRMessagingLoggerDebug(
kFIRMessagingMessageCodeSecureSocket007, @"%@ error occurred",
stream == self.inStream
? @"Input stream"
: (stream == self.outStream ? @"Output stream" : @"Unknown stream"));
[self disconnect];
break;
}
case NSStreamEventHasSpaceAvailable:
if (self.state != kFIRMessagingSecureSocketOpen) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket008,
@"Try to write to socket that is not opened");
return;
}
_FIRMessagingDevAssert(stream == self.outStream, @"Incorrect stream");
[self performWrite];
break;
default:
break;
}
}
#pragma mark - Private
- (void)openStream:(NSStream *)stream isVOIPStream:(BOOL)isVOIPStream {
_FIRMessagingDevAssert(stream != nil, @"Invalid stream");
_FIRMessagingDevAssert(self.runLoop != nil, @"Invalid runloop");
if (stream) {
_FIRMessagingDevAssert([stream streamStatus] == NSStreamStatusNotOpen, @"Stream already open");
if ([stream streamStatus] != NSStreamStatusNotOpen) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket009,
@"stream should not be open.");
return;
}
[stream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL
forKey:NSStreamSocketSecurityLevelKey];
if (isVOIPStream) {
[stream setProperty:NSStreamNetworkServiceTypeVoIP
forKey:NSStreamNetworkServiceType];
}
stream.delegate = self;
[stream scheduleInRunLoop:self.runLoop forMode:NSDefaultRunLoopMode];
[stream open];
}
}
- (void)closeStream:(NSStream *)stream {
_FIRMessagingDevAssert(stream != nil, @"Invalid stream");
_FIRMessagingDevAssert(self.runLoop != nil, @"Invalid runloop");
if (stream) {
[stream close];
[stream removeFromRunLoop:self.runLoop forMode:NSDefaultRunLoopMode];
stream.delegate = nil;
}
}
- (BOOL)performRead {
_FIRMessagingDevAssert(self.state == kFIRMessagingSecureSocketOpen, @"Socket should be open");
if (!self.isVersionReceived) {
self.isVersionReceived = YES;
uint8_t versionByte = 0;
NSInteger bytesRead = [self.inStream read:&versionByte maxLength:sizeof(uint8_t)];
if (bytesRead != sizeof(uint8_t) || kVersion != versionByte) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket010,
@"Version do not match. Received %d, Expecting %d", versionByte,
kVersion);
return NO;
}
}
while (YES) {
BOOL isInputBufferValid = [self.inputBuffer length] > 0;
_FIRMessagingDevAssert(isInputBufferValid,
@"Invalid input buffer size %lu. Used bytes length %lu, buffer content: %@",
_FIRMessaging_UL([self.inputBuffer length]),
_FIRMessaging_UL(self.inputBufferLength),
self.inputBuffer);
if (!isInputBufferValid) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket011,
@"Input buffer is not valid.");
return NO;
}
if (![self.inStream hasBytesAvailable]) {
break;
}
// try to read more data
uint8_t *unusedBufferPtr = (uint8_t *)self.inputBuffer.mutableBytes + self.inputBufferLength;
NSUInteger unusedBufferLength = [self.inputBuffer length] - self.inputBufferLength;
NSInteger bytesRead = [self.inStream read:unusedBufferPtr maxLength:unusedBufferLength];
if (bytesRead <= 0) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket012,
@"Failed to read input stream. Bytes read %ld, Used buffer size %lu, "
@"Unused buffer size %lu",
_FIRMessaging_UL(bytesRead), _FIRMessaging_UL(self.inputBufferLength),
_FIRMessaging_UL(unusedBufferLength));
break;
}
// did successfully read some more data
self.inputBufferLength += (NSUInteger)bytesRead;
if ([self.inputBuffer length] <= self.inputBufferLength) {
// shouldn't be reading more than 1MB of data in one go
if ([self.inputBuffer length] + kBufferLengthIncrement > kMaxBufferLength) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket013,
@"Input buffer exceed 1M, disconnect socket");
return NO;
}
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket014,
@"Input buffer limit exceeded. Used input buffer size %lu, "
@"Total input buffer size %lu. No unused buffer left. "
@"Increase buffer size.",
_FIRMessaging_UL(self.inputBufferLength),
_FIRMessaging_UL([self.inputBuffer length]));
[self.inputBuffer increaseLengthBy:kBufferLengthIncrement];
_FIRMessagingDevAssert([self.inputBuffer length] > self.inputBufferLength, @"Invalid buffer size");
}
while (self.inputBufferLength > 0 && [self.inputBuffer length] > 0) {
_FIRMessagingDevAssert([self.inputBuffer length] >= self.inputBufferLength,
@"Buffer longer than length");
NSRange inputRange = NSMakeRange(0, self.inputBufferLength);
size_t protoBytes = 0;
// read the actual proto data coming in
FIRMessagingSecureSocketReadResult readResult =
[self processCurrentInputBuffer:[self.inputBuffer subdataWithRange:inputRange]
outOffset:&protoBytes];
// Corrupt data encountered, stop processing.
if (readResult == kFIRMessagingSecureSocketReadResultCorrupt) {
return NO;
// Incomplete data, keep trying to read by loading more from the stream.
} else if (readResult == kFIRMessagingSecureSocketReadResultIncomplete) {
break;
}
_FIRMessagingDevAssert(self.inputBufferLength >= protoBytes, @"More bytes than buffer can handle");
// we have read (0, protoBytes) of data in the inputBuffer
if (protoBytes == self.inputBufferLength) {
// did completely read the buffer data can be reset for further processing
self.inputBufferLength = 0;
} else {
// delete processed bytes while maintaining the buffer size.
NSUInteger prevLength __unused = [self.inputBuffer length];
// delete the processed bytes
[self.inputBuffer replaceBytesInRange:NSMakeRange(0, protoBytes) withBytes:NULL length:0];
// reallocate more data
[self.inputBuffer increaseLengthBy:protoBytes];
_FIRMessagingDevAssert([self.inputBuffer length] == prevLength,
@"Invalid input buffer size %lu. Used bytes length %lu, "
@"buffer content: %@",
_FIRMessaging_UL([self.inputBuffer length]),
_FIRMessaging_UL(self.inputBufferLength),
self.inputBuffer);
self.inputBufferLength -= protoBytes;
}
}
}
return YES;
}
- (FIRMessagingSecureSocketReadResult)processCurrentInputBuffer:(NSData *)readData
outOffset:(size_t *)outOffset {
*outOffset = 0;
FIRMessagingCodedInputStream *input = [[FIRMessagingCodedInputStream alloc] initWithData:readData];
int8_t rawTag;
if (![input readTag:&rawTag]) {
return kFIRMessagingSecureSocketReadResultIncomplete;
}
int32_t length;
if (![input readLength:&length]) {
return kFIRMessagingSecureSocketReadResultIncomplete;
}
// NOTE tag can be zero for |HeartbeatPing|, and length can be zero for |Close| proto
_FIRMessagingDevAssert(rawTag >= 0 && length >= 0, @"Invalid tag or length");
if (rawTag < 0 || length < 0) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket015, @"Buffer data corrupted.");
return kFIRMessagingSecureSocketReadResultCorrupt;
}
NSData *data = [input readDataWithLength:(uint32_t)length];
if (data == nil) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSecureSocket016,
@"Incomplete data, buffered data length %ld, expected length %d",
_FIRMessaging_UL(self.inputBufferLength), length);
return kFIRMessagingSecureSocketReadResultIncomplete;
}
[self.delegate secureSocket:self didReceiveData:data withTag:rawTag];
*outOffset = input.offset;
return kFIRMessagingSecureSocketReadResultSuccess;
}
- (void)performWrite {
_FIRMessagingDevAssert(self.state == kFIRMessagingSecureSocketOpen, @"Invalid socket state");
if (!self.isVersionSent) {
self.isVersionSent = YES;
uint8_t versionByte = kVersion;
[self.outStream write:&versionByte maxLength:sizeof(uint8_t)];
}
while (!self.packetQueue.isEmpty && self.outStream.hasSpaceAvailable) {
if (self.outputBuffer.length == 0) {
// serialize new packets only when the output buffer is flushed.
FIRMessagingPacket *packet = [self.packetQueue pop];
self.currentRmqIdBeingSent = packet.rmqId;
self.currentProtoTypeBeingSent = packet.tag;
NSUInteger length = SerializedSize(packet.tag) +
SerializedSize((int)packet.data.length) + packet.data.length;
self.outputBuffer = [NSMutableData dataWithLength:length];
GPBCodedOutputStream *output = [GPBCodedOutputStream streamWithData:self.outputBuffer];
[output writeRawVarint32:packet.tag];
[output writeBytesNoTag:packet.data];
self.outputBufferLength = 0;
}
// flush the output buffer.
NSInteger written = [self.outStream write:self.outputBuffer.bytes + self.outputBufferLength
maxLength:self.outputBuffer.length - self.outputBufferLength];
if (written <= 0) {
continue;
}
self.outputBufferLength += (NSUInteger)written;
if (self.outputBufferLength >= self.outputBuffer.length) {
self.outputBufferLength = 0;
self.outputBuffer = nil;
[self.delegate secureSocket:self
didSendProtoWithTag:self.currentProtoTypeBeingSent
rmqId:self.currentRmqIdBeingSent];
self.currentRmqIdBeingSent = nil;
self.currentProtoTypeBeingSent = kInvalidTag;
}
}
}
@end
@@ -0,0 +1,59 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
@class FIRMessagingRmqManager;
/**
* Handle sync messages being received both via MCS and APNS.
*/
@interface FIRMessagingSyncMessageManager : NSObject
/**
* Initialize sync message manager.
*
* @param rmqManager The RMQ manager on the client.
*
* @return Sync message manager.
*/
- (instancetype)initWithRmqManager:(FIRMessagingRmqManager *)rmqManager;
/**
* Remove expired sync message from persistent store. Also removes messages that have
* been received both via APNS and MCS.
*/
- (void)removeExpiredSyncMessages;
/**
* App did recive a sync message via APNS.
*
* @param message The sync message received.
*
* @return YES if the message is a duplicate of an already received sync message else NO.
*/
- (BOOL)didReceiveAPNSSyncMessage:(NSDictionary *)message;
/**
* App did receive a sync message via MCS.
*
* @param message The sync message received.
*
* @return YES if the message is a duplicate of an already received sync message else NO.
*/
- (BOOL)didReceiveMCSSyncMessage:(NSDictionary *)message;
@end
@@ -0,0 +1,147 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessagingSyncMessageManager.h"
#import "FIRMessagingConstants.h"
#import "FIRMessagingDefines.h"
#import "FIRMessagingLogger.h"
#import "FIRMessagingPersistentSyncMessage.h"
#import "FIRMessagingRmqManager.h"
#import "FIRMessagingUtilities.h"
static const int64_t kDefaultSyncMessageTTL = 4 * 7 * 24 * 60 * 60; // 4 weeks
// 4 MB of free space is required to persist Sync messages
static const uint64_t kMinFreeDiskSpaceInMB = 1;
@interface FIRMessagingSyncMessageManager()
@property(nonatomic, readwrite, strong) FIRMessagingRmqManager *rmqManager;
@end
@implementation FIRMessagingSyncMessageManager
- (instancetype)init {
FIRMessagingInvalidateInitializer();
}
- (instancetype)initWithRmqManager:(FIRMessagingRmqManager *)rmqManager {
_FIRMessagingDevAssert(rmqManager, @"Invalid nil rmq manager while initalizing sync message manager");
self = [super init];
if (self) {
_rmqManager = rmqManager;
}
return self;
}
- (void)removeExpiredSyncMessages {
NSError *error;
int deleteCount = [self.rmqManager deleteExpiredOrFinishedSyncMessages:&error];
if (error) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeSyncMessageManager000,
@"Error while deleting expired sync messages %@", error);
} else if (deleteCount > 0) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeSyncMessageManager001,
@"Successfully deleted %d sync messages from store", deleteCount);
}
}
- (BOOL)didReceiveAPNSSyncMessage:(NSDictionary *)message {
return [self didReceiveSyncMessage:message viaAPNS:YES viaMCS:NO];
}
- (BOOL)didReceiveMCSSyncMessage:(NSDictionary *)message {
return [self didReceiveSyncMessage:message viaAPNS:NO viaMCS:YES];
}
- (BOOL)didReceiveSyncMessage:(NSDictionary *)message
viaAPNS:(BOOL)viaAPNS
viaMCS:(BOOL)viaMCS {
NSString *rmqID = message[kFIRMessagingMessageIDKey];
_FIRMessagingDevAssert([rmqID length], @"Invalid nil rmqID for message");
if (![rmqID length]) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeSyncMessageManager002,
@"Invalid nil rmqID for sync message.");
return NO;
}
FIRMessagingPersistentSyncMessage *persistentMessage =
[self.rmqManager querySyncMessageWithRmqID:rmqID];
NSError *error;
if (!persistentMessage) {
// Do not persist the new message if we don't have enough disk space
uint64_t freeDiskSpace = FIRMessagingGetFreeDiskSpaceInMB();
if (freeDiskSpace < kMinFreeDiskSpaceInMB) {
return NO;
}
int64_t expirationTime = [[self class] expirationTimeForSyncMessage:message];
if (![self.rmqManager saveSyncMessageWithRmqID:rmqID
expirationTime:expirationTime
apnsReceived:viaAPNS
mcsReceived:viaMCS
error:&error]) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeSyncMessageManager003,
@"Failed to save sync message with rmqID %@", rmqID);
} else {
FIRMessagingLoggerInfo(kFIRMessagingMessageCodeSyncMessageManager004,
@"Added sync message to cache: %@", rmqID);
}
return NO;
}
if (viaAPNS && !persistentMessage.apnsReceived) {
persistentMessage.apnsReceived = YES;
if (![self.rmqManager updateSyncMessageViaAPNSWithRmqID:rmqID error:&error]) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeSyncMessageManager005,
@"Failed to update APNS state for sync message %@", rmqID);
}
} else if (viaMCS && !persistentMessage.mcsReceived) {
persistentMessage.mcsReceived = YES;
if (![self.rmqManager updateSyncMessageViaMCSWithRmqID:rmqID error:&error]) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeSyncMessageManager006,
@"Failed to update MCS state for sync message %@", rmqID);
}
}
// Received message via both ways we can safely delete it.
if (persistentMessage.apnsReceived && persistentMessage.mcsReceived) {
if (![self.rmqManager deleteSyncMessageWithRmqID:rmqID]) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeSyncMessageManager007,
@"Failed to delete sync message %@", rmqID);
} else {
FIRMessagingLoggerInfo(kFIRMessagingMessageCodeSyncMessageManager008,
@"Successfully deleted sync message from cache %@", rmqID);
}
}
// Already received this message either via MCS or APNS.
return YES;
}
+ (int64_t)expirationTimeForSyncMessage:(NSDictionary *)message {
int64_t ttl = kDefaultSyncMessageTTL;
if (message[kFIRMessagingMessageSyncMessageTTLKey]) {
ttl = [message[kFIRMessagingMessageSyncMessageTTLKey] longLongValue];
}
int64_t currentTime = FIRMessagingCurrentTimestampInSeconds();
return currentTime + ttl;
}
@end
@@ -0,0 +1,43 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
#import "FIRMessaging.h"
#import "FIRMessagingTopicsCommon.h"
NS_ASSUME_NONNULL_BEGIN
/**
* An asynchronous NSOperation subclass which performs a single network request for a topic
* subscription operation. Once completed, it calls its provided completion handler.
*/
@interface FIRMessagingTopicOperation : NSOperation
@property(nonatomic, readonly, copy) NSString *topic;
@property(nonatomic, readonly, assign) FIRMessagingTopicAction action;
@property(nonatomic, readonly, copy) NSString *token;
@property(nonatomic, readonly, copy, nullable) NSDictionary *options;
- (instancetype)initWithTopic:(NSString *)topic
action:(FIRMessagingTopicAction)action
token:(NSString *)token
options:(nullable NSDictionary *)options
completion:(FIRMessagingTopicOperationCompletion)completion;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,260 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessagingTopicOperation.h"
#import <FirebaseInstanceID/FIRInstanceID_Private.h>
#import "FIRMessagingDefines.h"
#import "FIRMessagingLogger.h"
#import "FIRMessagingUtilities.h"
#import "NSError+FIRMessaging.h"
#define DEBUG_LOG_SUBSCRIPTION_OPERATION_DURATIONS 0
static NSString *const kFIRMessagingSubscribeServerHost =
@"https://iid.googleapis.com/iid/register";
NSString *FIRMessagingSubscriptionsServer() {
static NSString *serverHost = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSDictionary *environment = [[NSProcessInfo processInfo] environment];
NSString *customServerHost = environment[@"FCM_SERVER_ENDPOINT"];
if (customServerHost.length) {
serverHost = customServerHost;
} else {
serverHost = kFIRMessagingSubscribeServerHost;
}
});
return serverHost;
}
@interface FIRMessagingTopicOperation () {
BOOL _isFinished;
BOOL _isExecuting;
}
@property(nonatomic, readwrite, copy) NSString *topic;
@property(nonatomic, readwrite, assign) FIRMessagingTopicAction action;
@property(nonatomic, readwrite, copy) NSString *token;
@property(nonatomic, readwrite, copy) NSDictionary *options;
@property(nonatomic, readwrite, copy) FIRMessagingTopicOperationCompletion completion;
@property(atomic, strong) NSURLSessionDataTask *dataTask;
@end
@implementation FIRMessagingTopicOperation
+ (NSURLSession *)sharedSession {
static NSURLSession *subscriptionOperationSharedSession;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.timeoutIntervalForResource = 60.0f; // 1 minute
subscriptionOperationSharedSession = [NSURLSession sessionWithConfiguration:config];
subscriptionOperationSharedSession.sessionDescription = @"com.google.fcm.topics.session";
});
return subscriptionOperationSharedSession;
}
- (instancetype)initWithTopic:(NSString *)topic
action:(FIRMessagingTopicAction)action
token:(NSString *)token
options:(NSDictionary *)options
completion:(FIRMessagingTopicOperationCompletion)completion {
if (self = [super init]) {
_topic = topic;
_action = action;
_token = token;
_options = options;
_completion = completion;
_isExecuting = NO;
_isFinished = NO;
}
return self;
}
- (void)dealloc {
_topic = nil;
_token = nil;
_completion = nil;
}
- (BOOL)isAsynchronous {
return YES;
}
- (BOOL)isExecuting {
return _isExecuting;
}
- (void)setExecuting:(BOOL)executing {
[self willChangeValueForKey:@"isExecuting"];
_isExecuting = executing;
[self didChangeValueForKey:@"isExecuting"];
}
- (BOOL)isFinished {
return _isFinished;
}
- (void)setFinished:(BOOL)finished {
[self willChangeValueForKey:@"isFinished"];
_isFinished = finished;
[self didChangeValueForKey:@"isFinished"];
}
- (void)start {
if (self.isCancelled) {
NSError *error =
[NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubOperationIsCancelled];
[self finishWithError:error];
return;
}
[self setExecuting:YES];
[self performSubscriptionChange];
}
- (void)finishWithError:(NSError *)error {
// Add a check to prevent this finish from being called more than once.
if (self.isFinished) {
return;
}
self.dataTask = nil;
if (self.completion) {
self.completion(error);
}
[self setExecuting:NO];
[self setFinished:YES];
}
- (void)cancel {
[super cancel];
[self.dataTask cancel];
NSError *error = [NSError errorWithFCMErrorCode:kFIRMessagingErrorCodePubSubOperationIsCancelled];
[self finishWithError:error];
}
- (void)performSubscriptionChange {
NSURL *url = [NSURL URLWithString:FIRMessagingSubscriptionsServer()];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
NSString *appIdentifier = FIRMessagingAppIdentifier();
NSString *deviceAuthID = [FIRInstanceID instanceID].deviceAuthID;
NSString *secretToken = [FIRInstanceID instanceID].secretToken;
NSString *authString = [NSString stringWithFormat:@"AidLogin %@:%@", deviceAuthID, secretToken];
[request setValue:authString forHTTPHeaderField:@"Authorization"];
[request setValue:appIdentifier forHTTPHeaderField:@"app"];
[request setValue:[FIRInstanceID instanceID].versionInfo forHTTPHeaderField:@"info"];
// Topic can contain special characters (like `%`) so encode the value.
NSCharacterSet *characterSet = [NSCharacterSet URLQueryAllowedCharacterSet];
NSString *encodedTopic =
[self.topic stringByAddingPercentEncodingWithAllowedCharacters:characterSet];
if (encodedTopic == nil) {
// The transformation was somehow not possible, so use the original topic.
FIRMessagingLoggerWarn(kFIRMessagingMessageCodeTopicOptionTopicEncodingFailed,
@"Unable to encode the topic '%@' during topic subscription change. "
@"Please ensure that the topic name contains only valid characters.",
self.topic);
encodedTopic = self.topic;
}
NSMutableString *content = [NSMutableString stringWithFormat:
@"sender=%@&app=%@&device=%@&"
@"app_ver=%@&X-gcm.topic=%@&X-scope=%@",
self.token,
appIdentifier,
deviceAuthID,
FIRMessagingCurrentAppVersion(),
encodedTopic,
encodedTopic];
if (self.action == FIRMessagingTopicActionUnsubscribe) {
[content appendString:@"&delete=true"];
}
FIRMessagingLoggerInfo(kFIRMessagingMessageCodeTopicOption000, @"Topic subscription request: %@",
content);
request.HTTPBody = [content dataUsingEncoding:NSUTF8StringEncoding];
[request setHTTPMethod:@"POST"];
#if DEBUG_LOG_SUBSCRIPTION_OPERATION_DURATIONS
NSDate *start = [NSDate date];
#endif
FIRMessaging_WEAKIFY(self)
void(^requestHandler)(NSData *, NSURLResponse *, NSError *) =
^(NSData *data, NSURLResponse *URLResponse, NSError *error) {
FIRMessaging_STRONGIFY(self)
if (error) {
// Our operation could have been cancelled, which would result in our data task's error being
// NSURLErrorCancelled
if (error.code == NSURLErrorCancelled) {
// We would only have been cancelled in the -cancel method, which will call finish for us
// so just return and do nothing.
return;
}
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTopicOption001,
@"Device registration HTTP fetch error. Error Code: %ld",
_FIRMessaging_L(error.code));
[self finishWithError:error];
return;
}
NSString *response = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (response.length == 0) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTopicOperationEmptyResponse,
@"Invalid registration response - zero length.");
[self finishWithError:[NSError errorWithFCMErrorCode:kFIRMessagingErrorCodeUnknown]];
return;
}
NSArray *parts = [response componentsSeparatedByString:@"="];
_FIRMessagingDevAssert(parts.count, @"Invalid registration response");
if (![parts[0] isEqualToString:@"token"] || parts.count <= 1) {
FIRMessagingLoggerDebug(kFIRMessagingMessageCodeTopicOption002,
@"Invalid registration response %@", response);
[self finishWithError:[NSError errorWithFCMErrorCode:kFIRMessagingErrorCodeUnknown]];
return;
}
#if DEBUG_LOG_SUBSCRIPTION_OPERATION_DURATIONS
NSTimeInterval duration = -[start timeIntervalSinceNow];
FIRMessagingLoggerDebug(@"%@ change took %.2fs", self.topic, duration);
#endif
[self finishWithError:nil];
};
NSURLSession *urlSession = [FIRMessagingTopicOperation sharedSession];
self.dataTask = [urlSession dataTaskWithRequest:request completionHandler:requestHandler];
NSString *description;
if (_action == FIRMessagingTopicActionSubscribe) {
description = [NSString stringWithFormat:@"com.google.fcm.topics.subscribe: %@", _topic];
} else {
description = [NSString stringWithFormat:@"com.google.fcm.topics.unsubscribe: %@", _topic];
}
self.dataTask.taskDescription = description;
[self.dataTask resume];
}
@end
@@ -0,0 +1,29 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/**
* Represents the action taken on a subscription topic.
*/
typedef NS_ENUM(NSInteger, FIRMessagingTopicAction) {
FIRMessagingTopicActionSubscribe,
FIRMessagingTopicActionUnsubscribe
};
NS_ASSUME_NONNULL_END
@@ -0,0 +1,59 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
typedef NS_ENUM(int8_t, FIRMessagingProtoTag) {
kFIRMessagingProtoTagInvalid = -1,
kFIRMessagingProtoTagHeartbeatPing = 0,
kFIRMessagingProtoTagHeartbeatAck = 1,
kFIRMessagingProtoTagLoginRequest = 2,
kFIRMessagingProtoTagLoginResponse = 3,
kFIRMessagingProtoTagClose = 4,
kFIRMessagingProtoTagIqStanza = 7,
kFIRMessagingProtoTagDataMessageStanza = 8,
};
@class GPBMessage;
#pragma mark - Protocol Buffers
FOUNDATION_EXPORT FIRMessagingProtoTag FIRMessagingGetTagForProto(GPBMessage *protoClass);
FOUNDATION_EXPORT Class FIRMessagingGetClassForTag(FIRMessagingProtoTag tag);
#pragma mark - MCS
FOUNDATION_EXPORT NSString *FIRMessagingGetRmq2Id(GPBMessage *proto);
FOUNDATION_EXPORT void FIRMessagingSetRmq2Id(GPBMessage *proto, NSString *pID);
FOUNDATION_EXPORT int FIRMessagingGetLastStreamId(GPBMessage *proto);
FOUNDATION_EXPORT void FIRMessagingSetLastStreamId(GPBMessage *proto, int sid);
#pragma mark - Time
FOUNDATION_EXPORT int64_t FIRMessagingCurrentTimestampInSeconds(void);
FOUNDATION_EXPORT int64_t FIRMessagingCurrentTimestampInMilliseconds(void);
#pragma mark - App Info
FOUNDATION_EXPORT NSString *FIRMessagingCurrentAppVersion(void);
FOUNDATION_EXPORT NSString *FIRMessagingAppIdentifier(void);
#pragma mark - Others
FOUNDATION_EXPORT uint64_t FIRMessagingGetFreeDiskSpaceInMB(void);
FOUNDATION_EXPORT NSSearchPathDirectory FIRMessagingSupportedDirectory(void);
@@ -0,0 +1,183 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessagingUtilities.h"
#import "Protos/GtalkCore.pbobjc.h"
#import "FIRMessagingLogger.h"
#import <GoogleUtilities/GULAppEnvironmentUtil.h>
// Convert the macro to a string
#define STR_EXPAND(x) #x
#define STR(x) STR_EXPAND(x)
static const uint64_t kBytesToMegabytesDivisor = 1024 * 1024LL;
#pragma mark - Protocol Buffers
FIRMessagingProtoTag FIRMessagingGetTagForProto(GPBMessage *proto) {
if ([proto isKindOfClass:[GtalkHeartbeatPing class]]) {
return kFIRMessagingProtoTagHeartbeatPing;
} else if ([proto isKindOfClass:[GtalkHeartbeatAck class]]) {
return kFIRMessagingProtoTagHeartbeatAck;
} else if ([proto isKindOfClass:[GtalkLoginRequest class]]) {
return kFIRMessagingProtoTagLoginRequest;
} else if ([proto isKindOfClass:[GtalkLoginResponse class]]) {
return kFIRMessagingProtoTagLoginResponse;
} else if ([proto isKindOfClass:[GtalkClose class]]) {
return kFIRMessagingProtoTagClose;
} else if ([proto isKindOfClass:[GtalkIqStanza class]]) {
return kFIRMessagingProtoTagIqStanza;
} else if ([proto isKindOfClass:[GtalkDataMessageStanza class]]) {
return kFIRMessagingProtoTagDataMessageStanza;
}
return kFIRMessagingProtoTagInvalid;
}
Class FIRMessagingGetClassForTag(FIRMessagingProtoTag tag) {
switch (tag) {
case kFIRMessagingProtoTagHeartbeatPing:
return GtalkHeartbeatPing.class;
case kFIRMessagingProtoTagHeartbeatAck:
return GtalkHeartbeatAck.class;
case kFIRMessagingProtoTagLoginRequest:
return GtalkLoginRequest.class;
case kFIRMessagingProtoTagLoginResponse:
return GtalkLoginResponse.class;
case kFIRMessagingProtoTagClose:
return GtalkClose.class;
case kFIRMessagingProtoTagIqStanza:
return GtalkIqStanza.class;
case kFIRMessagingProtoTagDataMessageStanza:
return GtalkDataMessageStanza.class;
case kFIRMessagingProtoTagInvalid:
return NSNull.class;
}
return NSNull.class;
}
#pragma mark - MCS
NSString *FIRMessagingGetRmq2Id(GPBMessage *proto) {
if ([proto isKindOfClass:[GtalkIqStanza class]]) {
if (((GtalkIqStanza *)proto).hasPersistentId) {
return ((GtalkIqStanza *)proto).persistentId;
}
} else if ([proto isKindOfClass:[GtalkDataMessageStanza class]]) {
if (((GtalkDataMessageStanza *)proto).hasPersistentId) {
return ((GtalkDataMessageStanza *)proto).persistentId;
}
}
return nil;
}
void FIRMessagingSetRmq2Id(GPBMessage *proto, NSString *pID) {
if ([proto isKindOfClass:[GtalkIqStanza class]]) {
((GtalkIqStanza *)proto).persistentId = pID;
} else if ([proto isKindOfClass:[GtalkDataMessageStanza class]]) {
((GtalkDataMessageStanza *)proto).persistentId = pID;
}
}
int FIRMessagingGetLastStreamId(GPBMessage *proto) {
if ([proto isKindOfClass:[GtalkIqStanza class]]) {
if (((GtalkIqStanza *)proto).hasLastStreamIdReceived) {
return ((GtalkIqStanza *)proto).lastStreamIdReceived;
}
} else if ([proto isKindOfClass:[GtalkDataMessageStanza class]]) {
if (((GtalkDataMessageStanza *)proto).hasLastStreamIdReceived) {
return ((GtalkDataMessageStanza *)proto).lastStreamIdReceived;
}
} else if ([proto isKindOfClass:[GtalkHeartbeatPing class]]) {
if (((GtalkHeartbeatPing *)proto).hasLastStreamIdReceived) {
return ((GtalkHeartbeatPing *)proto).lastStreamIdReceived;
}
} else if ([proto isKindOfClass:[GtalkHeartbeatAck class]]) {
if (((GtalkHeartbeatAck *)proto).hasLastStreamIdReceived) {
return ((GtalkHeartbeatAck *)proto).lastStreamIdReceived;
}
}
return -1;
}
void FIRMessagingSetLastStreamId(GPBMessage *proto, int sid) {
if ([proto isKindOfClass:[GtalkIqStanza class]]) {
((GtalkIqStanza *)proto).lastStreamIdReceived = sid;
} else if ([proto isKindOfClass:[GtalkDataMessageStanza class]]) {
((GtalkDataMessageStanza *)proto).lastStreamIdReceived = sid;
} else if ([proto isKindOfClass:[GtalkHeartbeatPing class]]) {
((GtalkHeartbeatPing *)proto).lastStreamIdReceived = sid;
} else if ([proto isKindOfClass:[GtalkHeartbeatAck class]]) {
((GtalkHeartbeatAck *)proto).lastStreamIdReceived = sid;
}
}
#pragma mark - Time
int64_t FIRMessagingCurrentTimestampInSeconds(void) {
return (int64_t)[[NSDate date] timeIntervalSince1970];
}
int64_t FIRMessagingCurrentTimestampInMilliseconds(void) {
return (int64_t)(FIRMessagingCurrentTimestampInSeconds() * 1000.0);
}
#pragma mark - App Info
NSString *FIRMessagingCurrentAppVersion(void) {
NSString *version = [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"];
if (![version length]) {
FIRMessagingLoggerError(kFIRMessagingMessageCodeUtilities000,
@"Could not find current app version");
return @"";
}
return version;
}
NSString *FIRMessagingAppIdentifier(void) {
return [[NSBundle mainBundle] bundleIdentifier];
}
uint64_t FIRMessagingGetFreeDiskSpaceInMB(void) {
NSError *error;
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSDictionary *attributesMap =
[[NSFileManager defaultManager] attributesOfFileSystemForPath:[paths lastObject]
error:&error];
if (attributesMap) {
uint64_t totalSizeInBytes __unused = [attributesMap[NSFileSystemSize] longLongValue];
uint64_t freeSizeInBytes = [attributesMap[NSFileSystemFreeSize] longLongValue];
FIRMessagingLoggerDebug(
kFIRMessagingMessageCodeUtilities001, @"Device has capacity %llu MB with %llu MB free.",
totalSizeInBytes / kBytesToMegabytesDivisor, freeSizeInBytes / kBytesToMegabytesDivisor);
return ((double)freeSizeInBytes) / kBytesToMegabytesDivisor;
} else {
FIRMessagingLoggerError(kFIRMessagingMessageCodeUtilities002,
@"Error in retreiving device's free memory %@", error);
return 0;
}
}
NSSearchPathDirectory FIRMessagingSupportedDirectory(void) {
#if TARGET_OS_TV
return NSCachesDirectory;
#else
return NSApplicationSupportDirectory;
#endif
}
@@ -0,0 +1,35 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
/**
* Parsing utility for FIRMessaging Library versions. FIRMessaging Library follows semantic versioning.
* This provides utilities to parse the library versions to enable features and do
* updates based on appropriate library versions.
*
* Some example semantic versions are 1.0.1, 2.1.0, 2.1.1, 2.2.0-alpha1, 2.2.1-beta1
*/
FOUNDATION_EXPORT NSString *FIRMessagingCurrentLibraryVersion(void);
/// Returns the current Major version of FIRMessaging library.
FOUNDATION_EXPORT int FIRMessagingCurrentLibraryVersionMajor(void);
/// Returns the current Minor version of FIRMessaging library.
FOUNDATION_EXPORT int FIRMessagingCurrentLibraryVersionMinor(void);
/// Returns the current Patch version of FIRMessaging library.
FOUNDATION_EXPORT int FIRMessagingCurrentLibraryVersionPatch(void);
/// Returns YES if current library version is `beta` else NO.
FOUNDATION_EXPORT BOOL FIRMessagingCurrentLibraryVersionIsBeta(void);
@@ -0,0 +1,87 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessagingVersionUtilities.h"
#import "FIRMessagingDefines.h"
// Convert the macro to a string
#define STR_EXPAND(x) #x
#define STR(x) STR_EXPAND(x)
static NSString *const kSemanticVersioningSeparator = @".";
static NSString *const kBetaVersionPrefix = @"-beta";
static NSString *libraryVersion;
static int majorVersion;
static int minorVersion;
static int patchVersion;
static int betaVersion;
void FIRMessagingParseCurrentLibraryVersion(void) {
static NSArray *allVersions;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSMutableString *daylightVersion = [NSMutableString stringWithUTF8String:STR(FIRMessaging_LIB_VERSION)];
// Parse versions
// major, minor, patch[-beta#]
allVersions = [daylightVersion componentsSeparatedByString:kSemanticVersioningSeparator];
_FIRMessagingDevAssert(allVersions.count == 3, @"Invalid versioning of FIRMessaging library");
if (allVersions.count == 3) {
majorVersion = [allVersions[0] intValue];
minorVersion = [allVersions[1] intValue];
// Parse patch and beta versions
NSArray *patchAndBetaVersion =
[allVersions[2] componentsSeparatedByString:kBetaVersionPrefix];
_FIRMessagingDevAssert(patchAndBetaVersion.count <= 2, @"Invalid versioning of FIRMessaging library");
if (patchAndBetaVersion.count == 2) {
patchVersion = [patchAndBetaVersion[0] intValue];
betaVersion = [patchAndBetaVersion[1] intValue];
} else if (patchAndBetaVersion.count == 1) {
patchVersion = [patchAndBetaVersion[0] intValue];
}
}
// Copy library version
libraryVersion = [daylightVersion copy];
});
}
NSString *FIRMessagingCurrentLibraryVersion(void) {
FIRMessagingParseCurrentLibraryVersion();
return libraryVersion;
}
int FIRMessagingCurrentLibraryVersionMajor(void) {
FIRMessagingParseCurrentLibraryVersion();
return majorVersion;
}
int FIRMessagingCurrentLibraryVersionMinor(void) {
FIRMessagingParseCurrentLibraryVersion();
return minorVersion;
}
int FIRMessagingCurrentLibraryVersionPatch(void) {
FIRMessagingParseCurrentLibraryVersion();
return patchVersion;
}
BOOL FIRMessagingCurrentLibraryVersionIsBeta(void) {
FIRMessagingParseCurrentLibraryVersion();
return betaVersion > 0;
}
@@ -0,0 +1,59 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessaging.h"
@class FIRMessagingClient;
@class FIRMessagingPubSub;
typedef NS_ENUM(int8_t, FIRMessagingNetworkStatus) {
kFIRMessagingReachabilityNotReachable = 0,
kFIRMessagingReachabilityReachableViaWiFi,
kFIRMessagingReachabilityReachableViaWWAN,
};
FOUNDATION_EXPORT NSString *const kFIRMessagingPlistAutoInitEnabled;
FOUNDATION_EXPORT NSString *const kFIRMessagingUserDefaultsKeyAutoInitEnabled;
FOUNDATION_EXPORT NSString *const kFIRMessagingUserDefaultsKeyUseMessagingDelegate;
FOUNDATION_EXPORT NSString *const kFIRMessagingPlistUseMessagingDelegate;
@interface FIRMessagingRemoteMessage ()
@property(nonatomic, copy) NSString *messageID;
@property(nonatomic, strong) NSDictionary *appData;
@end
@interface FIRMessaging ()
#pragma mark - Private API
- (NSString *)defaultFcmToken;
- (FIRMessagingClient *)client;
- (FIRMessagingPubSub *)pubsub;
// Create a sample message to be sent over the wire using FIRMessaging. Look at
// FIRMessagingService.h to see what each param signifies.
+ (NSMutableDictionary *)createFIRMessagingMessageWithMessage:(NSDictionary *)message
to:(NSString *)to
withID:(NSString *)msgID
timeToLive:(int64_t)ttl
delay:(int)delay;
- (BOOL)isNetworkAvailable;
- (FIRMessagingNetworkStatus)networkType;
@end
@@ -0,0 +1,17 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessaging.h"
@@ -0,0 +1,30 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// @file FIRMessagingInternalUtilities.h
///
/// Internal Class Names and Methods that other libs can query at runtime.
/// FIRMessaging Class that responds to the FIRMessaging SDK version selector.
/// Verify at runtime if the class exists and implements the
/// required method.
static NSString *const kFIRMessagingSDKClassString = @"FIRMessaging";
/// FIRMessaging selector that returns the current FIRMessaging library version.
static NSString *const kFIRMessagingSDKVersionSelectorString = @"FIRMessagingSDKVersion";
/// FIRMessaging selector that returns the current device locale.
static NSString *const kFIRMessagingSDKLocaleSelectorString = @"FIRMessagingSDKCurrentLocale";
@@ -0,0 +1,45 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
@interface NSDictionary (FIRMessaging)
/**
* Returns a string representation for the given dictionary. Assumes that all
* keys and values are strings.
*
* @return A string representation of all keys and values in the dictionary.
* The returned string is not pretty-printed.
*/
- (NSString *)fcm_string;
/**
* Check if the dictionary has any non-string keys or values.
*
* @return YES if the dictionary has any non-string keys or values else NO.
*/
- (BOOL)fcm_hasNonStringKeysOrValues;
/**
* Trims all (key, value) pair in a dictionary that are not strings.
*
* @return A new copied dictionary with all the non-string keys or values
* removed from the original dictionary.
*/
- (NSDictionary *)fcm_trimNonStringValues;
@end
@@ -0,0 +1,59 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "NSDictionary+FIRMessaging.h"
@implementation NSDictionary (FIRMessaging)
- (NSString *)fcm_string {
NSMutableString *dictAsString = [NSMutableString string];
NSString *separator = @"|";
for (id key in self) {
id value = self[key];
if ([key isKindOfClass:[NSString class]] && [value isKindOfClass:[NSString class]]) {
[dictAsString appendFormat:@"%@:%@%@", key, value, separator];
}
}
// remove the last separator
if ([dictAsString length]) {
[dictAsString deleteCharactersInRange:NSMakeRange(dictAsString.length - 1, 1)];
}
return [dictAsString copy];
}
- (BOOL)fcm_hasNonStringKeysOrValues {
for (id key in self) {
id value = self[key];
if (![key isKindOfClass:[NSString class]] || ![value isKindOfClass:[NSString class]]) {
return YES;
}
}
return NO;
}
- (NSDictionary *)fcm_trimNonStringValues {
NSMutableDictionary *trimDictionary =
[NSMutableDictionary dictionaryWithCapacity:self.count];
for (id key in self) {
id value = self[key];
if ([key isKindOfClass:[NSString class]] && [value isKindOfClass:[NSString class]]) {
trimDictionary[(NSString *)key] = value;
}
}
return trimDictionary;
}
@end
@@ -0,0 +1,69 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
FOUNDATION_EXPORT NSString *const kFIRMessagingDomain;
typedef NS_ENUM(NSUInteger, FIRMessagingInternalErrorCode) {
// Unknown error.
kFIRMessagingErrorCodeUnknown = 0,
// HTTP related errors.
kFIRMessagingErrorCodeAuthentication = 1,
kFIRMessagingErrorCodeNoAccess = 2,
kFIRMessagingErrorCodeTimeout = 3,
kFIRMessagingErrorCodeNetwork = 4,
// Another operation is in progress.
kFIRMessagingErrorCodeOperationInProgress = 5,
// Failed to perform device check in.
kFIRMessagingErrorCodeRegistrarFailedToCheckIn = 6,
kFIRMessagingErrorCodeInvalidRequest = 7,
// FIRMessaging generic errors
kFIRMessagingErrorCodeMissingDeviceID = 501,
// upstream send errors
kFIRMessagingErrorServiceNotAvailable = 1001,
kFIRMessagingErrorInvalidParameters = 1002,
kFIRMessagingErrorMissingTo = 1003,
kFIRMessagingErrorSave = 1004,
kFIRMessagingErrorSizeExceeded = 1005,
// Future Send Errors
// MCS errors
// Already connected with MCS
kFIRMessagingErrorCodeAlreadyConnected = 2001,
// PubSub errors
kFIRMessagingErrorCodePubSubAlreadySubscribed = 3001,
kFIRMessagingErrorCodePubSubAlreadyUnsubscribed = 3002,
kFIRMessagingErrorCodePubSubInvalidTopic = 3003,
kFIRMessagingErrorCodePubSubFIRMessagingNotSetup = 3004,
kFIRMessagingErrorCodePubSubOperationIsCancelled = 3005,
};
@interface NSError (FIRMessaging)
@property(nonatomic, readonly) FIRMessagingInternalErrorCode fcmErrorCode;
+ (NSError *)errorWithFCMErrorCode:(FIRMessagingInternalErrorCode)fcmErrorCode;
+ (NSError *)fcm_errorWithCode:(NSInteger)code userInfo:(NSDictionary *)userInfo;
@end
@@ -0,0 +1,35 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "NSError+FIRMessaging.h"
NSString *const kFIRMessagingDomain = @"com.google.fcm";
@implementation NSError (FIRMessaging)
- (FIRMessagingInternalErrorCode)fcmErrorCode {
return (FIRMessagingInternalErrorCode)self.code;
}
+ (NSError *)errorWithFCMErrorCode:(FIRMessagingInternalErrorCode)fcmErrorCode {
return [NSError errorWithDomain:kFIRMessagingDomain code:fcmErrorCode userInfo:nil];
}
+ (NSError *)fcm_errorWithCode:(NSInteger)code userInfo:(NSDictionary *)userInfo {
return [NSError errorWithDomain:kFIRMessagingDomain code:code userInfo:userInfo];
}
@end
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,617 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Generated by the protocol buffer compiler. DO NOT EDIT!
// source: buzz/mobile/proto/gtalk_extensions.proto
// This CPP symbol can be defined to use imports that match up to the framework
// imports needed when using CocoaPods.
#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
#define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
#endif
#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
#import <Protobuf/GPBProtocolBuffers.h>
#else
#import "GPBProtocolBuffers.h"
#endif
#if GOOGLE_PROTOBUF_OBJC_VERSION < 30002
#error This file was generated by a newer version of protoc which is incompatible with your Protocol Buffer library sources.
#endif
#if 30002 < GOOGLE_PROTOBUF_OBJC_MIN_SUPPORTED_VERSION
#error This file was generated by an older version of protoc which is incompatible with your Protocol Buffer library sources.
#endif
// @@protoc_insertion_point(imports)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
CF_EXTERN_C_BEGIN
@class GtalkOtrItem;
@class GtalkPhoto;
@class GtalkRosterItem;
@class GtalkSharedStatus_StatusList;
NS_ASSUME_NONNULL_BEGIN
#pragma mark - Enum GtalkRosterItem_SubscriptionType
typedef GPB_ENUM(GtalkRosterItem_SubscriptionType) {
GtalkRosterItem_SubscriptionType_None = 0,
GtalkRosterItem_SubscriptionType_To = 1,
GtalkRosterItem_SubscriptionType_From = 2,
GtalkRosterItem_SubscriptionType_Both = 3,
GtalkRosterItem_SubscriptionType_Remove = 4,
};
GPBEnumDescriptor *GtalkRosterItem_SubscriptionType_EnumDescriptor(void);
/**
* Checks to see if the given value is defined by the enum or was not known at
* the time this source was generated.
**/
BOOL GtalkRosterItem_SubscriptionType_IsValidValue(int32_t value);
#pragma mark - Enum GtalkRosterItem_AskType
typedef GPB_ENUM(GtalkRosterItem_AskType) {
GtalkRosterItem_AskType_Subscribe = 0,
};
GPBEnumDescriptor *GtalkRosterItem_AskType_EnumDescriptor(void);
/**
* Checks to see if the given value is defined by the enum or was not known at
* the time this source was generated.
**/
BOOL GtalkRosterItem_AskType_IsValidValue(int32_t value);
#pragma mark - Enum GtalkRosterItem_DisplayType
typedef GPB_ENUM(GtalkRosterItem_DisplayType) {
GtalkRosterItem_DisplayType_Blocked = 0,
GtalkRosterItem_DisplayType_Hidden = 1,
GtalkRosterItem_DisplayType_Pinned = 2,
};
GPBEnumDescriptor *GtalkRosterItem_DisplayType_EnumDescriptor(void);
/**
* Checks to see if the given value is defined by the enum or was not known at
* the time this source was generated.
**/
BOOL GtalkRosterItem_DisplayType_IsValidValue(int32_t value);
#pragma mark - Enum GtalkSharedStatus_ShowType
typedef GPB_ENUM(GtalkSharedStatus_ShowType) {
GtalkSharedStatus_ShowType_Default = 0,
GtalkSharedStatus_ShowType_Dnd = 1,
};
GPBEnumDescriptor *GtalkSharedStatus_ShowType_EnumDescriptor(void);
/**
* Checks to see if the given value is defined by the enum or was not known at
* the time this source was generated.
**/
BOOL GtalkSharedStatus_ShowType_IsValidValue(int32_t value);
#pragma mark - Enum GtalkPostAuthBatchQuery_CapabilitiesExtFlags
typedef GPB_ENUM(GtalkPostAuthBatchQuery_CapabilitiesExtFlags) {
GtalkPostAuthBatchQuery_CapabilitiesExtFlags_HasVoiceV1 = 1,
GtalkPostAuthBatchQuery_CapabilitiesExtFlags_HasVideoV1 = 2,
GtalkPostAuthBatchQuery_CapabilitiesExtFlags_HasCameraV1 = 4,
GtalkPostAuthBatchQuery_CapabilitiesExtFlags_HasPmucV1 = 8,
};
GPBEnumDescriptor *GtalkPostAuthBatchQuery_CapabilitiesExtFlags_EnumDescriptor(void);
/**
* Checks to see if the given value is defined by the enum or was not known at
* the time this source was generated.
**/
BOOL GtalkPostAuthBatchQuery_CapabilitiesExtFlags_IsValidValue(int32_t value);
#pragma mark - GtalkGtalkExtensionsRoot
/**
* Exposes the extension registry for this file.
*
* The base class provides:
* @code
* + (GPBExtensionRegistry *)extensionRegistry;
* @endcode
* which is a @c GPBExtensionRegistry that includes all the extensions defined by
* this file and all files that it depends on.
**/
@interface GtalkGtalkExtensionsRoot : GPBRootObject
@end
#pragma mark - GtalkRosterQuery
typedef GPB_ENUM(GtalkRosterQuery_FieldNumber) {
GtalkRosterQuery_FieldNumber_Etag = 1,
GtalkRosterQuery_FieldNumber_NotModified = 2,
GtalkRosterQuery_FieldNumber_ItemArray = 3,
GtalkRosterQuery_FieldNumber_AvatarWidth = 4,
GtalkRosterQuery_FieldNumber_AvatarHeight = 5,
};
@interface GtalkRosterQuery : GPBMessage
@property(nonatomic, readwrite, copy, null_resettable) NSString *etag;
/** Test to see if @c etag has been set. */
@property(nonatomic, readwrite) BOOL hasEtag;
@property(nonatomic, readwrite) BOOL notModified;
@property(nonatomic, readwrite) BOOL hasNotModified;
@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GtalkRosterItem*> *itemArray;
/** The number of items in @c itemArray without causing the array to be created. */
@property(nonatomic, readonly) NSUInteger itemArray_Count;
@property(nonatomic, readwrite) int32_t avatarWidth;
@property(nonatomic, readwrite) BOOL hasAvatarWidth;
@property(nonatomic, readwrite) int32_t avatarHeight;
@property(nonatomic, readwrite) BOOL hasAvatarHeight;
@end
#pragma mark - GtalkRosterItem
typedef GPB_ENUM(GtalkRosterItem_FieldNumber) {
GtalkRosterItem_FieldNumber_Jid = 1,
GtalkRosterItem_FieldNumber_Name = 2,
GtalkRosterItem_FieldNumber_Subscription = 3,
GtalkRosterItem_FieldNumber_Ask = 4,
GtalkRosterItem_FieldNumber_GroupArray = 5,
GtalkRosterItem_FieldNumber_QuickContact = 6,
GtalkRosterItem_FieldNumber_Display = 7,
GtalkRosterItem_FieldNumber_Rejected = 8,
};
@interface GtalkRosterItem : GPBMessage
@property(nonatomic, readwrite, copy, null_resettable) NSString *jid;
/** Test to see if @c jid has been set. */
@property(nonatomic, readwrite) BOOL hasJid;
@property(nonatomic, readwrite, copy, null_resettable) NSString *name;
/** Test to see if @c name has been set. */
@property(nonatomic, readwrite) BOOL hasName;
@property(nonatomic, readwrite) GtalkRosterItem_SubscriptionType subscription;
@property(nonatomic, readwrite) BOOL hasSubscription;
@property(nonatomic, readwrite) GtalkRosterItem_AskType ask;
@property(nonatomic, readwrite) BOOL hasAsk;
@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<NSString*> *groupArray;
/** The number of items in @c groupArray without causing the array to be created. */
@property(nonatomic, readonly) NSUInteger groupArray_Count;
@property(nonatomic, readwrite) BOOL quickContact;
@property(nonatomic, readwrite) BOOL hasQuickContact;
@property(nonatomic, readwrite) GtalkRosterItem_DisplayType display;
@property(nonatomic, readwrite) BOOL hasDisplay;
@property(nonatomic, readwrite) BOOL rejected;
@property(nonatomic, readwrite) BOOL hasRejected;
@end
#pragma mark - GtalkRmqLastId
typedef GPB_ENUM(GtalkRmqLastId_FieldNumber) {
GtalkRmqLastId_FieldNumber_Id_p = 1,
};
@interface GtalkRmqLastId : GPBMessage
@property(nonatomic, readwrite) int64_t id_p;
@property(nonatomic, readwrite) BOOL hasId_p;
@end
#pragma mark - GtalkRmqAck
typedef GPB_ENUM(GtalkRmqAck_FieldNumber) {
GtalkRmqAck_FieldNumber_Id_p = 1,
};
@interface GtalkRmqAck : GPBMessage
@property(nonatomic, readwrite) int64_t id_p;
@property(nonatomic, readwrite) BOOL hasId_p;
@end
#pragma mark - GtalkVCard
typedef GPB_ENUM(GtalkVCard_FieldNumber) {
GtalkVCard_FieldNumber_Version = 1,
GtalkVCard_FieldNumber_FullName = 2,
GtalkVCard_FieldNumber_Photo = 3,
GtalkVCard_FieldNumber_AvatarHash = 4,
GtalkVCard_FieldNumber_Modified = 5,
};
@interface GtalkVCard : GPBMessage
@property(nonatomic, readwrite, copy, null_resettable) NSString *version;
/** Test to see if @c version has been set. */
@property(nonatomic, readwrite) BOOL hasVersion;
@property(nonatomic, readwrite, copy, null_resettable) NSString *fullName;
/** Test to see if @c fullName has been set. */
@property(nonatomic, readwrite) BOOL hasFullName;
@property(nonatomic, readwrite, strong, null_resettable) GtalkPhoto *photo;
/** Test to see if @c photo has been set. */
@property(nonatomic, readwrite) BOOL hasPhoto;
@property(nonatomic, readwrite, copy, null_resettable) NSString *avatarHash;
/** Test to see if @c avatarHash has been set. */
@property(nonatomic, readwrite) BOOL hasAvatarHash;
@property(nonatomic, readwrite) BOOL modified;
@property(nonatomic, readwrite) BOOL hasModified;
@end
#pragma mark - GtalkPhoto
typedef GPB_ENUM(GtalkPhoto_FieldNumber) {
GtalkPhoto_FieldNumber_Type = 1,
GtalkPhoto_FieldNumber_Data_p = 2,
};
@interface GtalkPhoto : GPBMessage
@property(nonatomic, readwrite, copy, null_resettable) NSString *type;
/** Test to see if @c type has been set. */
@property(nonatomic, readwrite) BOOL hasType;
@property(nonatomic, readwrite, copy, null_resettable) NSString *data_p;
/** Test to see if @c data_p has been set. */
@property(nonatomic, readwrite) BOOL hasData_p;
@end
#pragma mark - GtalkChatRead
typedef GPB_ENUM(GtalkChatRead_FieldNumber) {
GtalkChatRead_FieldNumber_User = 1,
};
@interface GtalkChatRead : GPBMessage
@property(nonatomic, readwrite, copy, null_resettable) NSString *user;
/** Test to see if @c user has been set. */
@property(nonatomic, readwrite) BOOL hasUser;
@end
#pragma mark - GtalkChatClosed
typedef GPB_ENUM(GtalkChatClosed_FieldNumber) {
GtalkChatClosed_FieldNumber_User = 1,
};
@interface GtalkChatClosed : GPBMessage
@property(nonatomic, readwrite, copy, null_resettable) NSString *user;
/** Test to see if @c user has been set. */
@property(nonatomic, readwrite) BOOL hasUser;
@end
#pragma mark - GtalkCapabilities
typedef GPB_ENUM(GtalkCapabilities_FieldNumber) {
GtalkCapabilities_FieldNumber_Node = 1,
GtalkCapabilities_FieldNumber_Ver = 2,
GtalkCapabilities_FieldNumber_Ext = 3,
GtalkCapabilities_FieldNumber_Hash_p = 4,
};
@interface GtalkCapabilities : GPBMessage
@property(nonatomic, readwrite, copy, null_resettable) NSString *node;
/** Test to see if @c node has been set. */
@property(nonatomic, readwrite) BOOL hasNode;
@property(nonatomic, readwrite, copy, null_resettable) NSString *ver;
/** Test to see if @c ver has been set. */
@property(nonatomic, readwrite) BOOL hasVer;
@property(nonatomic, readwrite, copy, null_resettable) NSString *ext;
/** Test to see if @c ext has been set. */
@property(nonatomic, readwrite) BOOL hasExt;
@property(nonatomic, readwrite, copy, null_resettable) NSString *hash_p;
/** Test to see if @c hash_p has been set. */
@property(nonatomic, readwrite) BOOL hasHash_p;
@end
#pragma mark - GtalkSharedStatus
typedef GPB_ENUM(GtalkSharedStatus_FieldNumber) {
GtalkSharedStatus_FieldNumber_StatusMax = 1,
GtalkSharedStatus_FieldNumber_StatusListMax = 2,
GtalkSharedStatus_FieldNumber_StatusListContentsMax = 3,
GtalkSharedStatus_FieldNumber_Status = 4,
GtalkSharedStatus_FieldNumber_Show = 5,
GtalkSharedStatus_FieldNumber_StatusListArray = 6,
GtalkSharedStatus_FieldNumber_Invisible = 9,
GtalkSharedStatus_FieldNumber_StatusMinVersion = 10,
};
@interface GtalkSharedStatus : GPBMessage
@property(nonatomic, readwrite) int32_t statusMax;
@property(nonatomic, readwrite) BOOL hasStatusMax;
@property(nonatomic, readwrite) int32_t statusListMax;
@property(nonatomic, readwrite) BOOL hasStatusListMax;
@property(nonatomic, readwrite) int32_t statusListContentsMax;
@property(nonatomic, readwrite) BOOL hasStatusListContentsMax;
@property(nonatomic, readwrite, copy, null_resettable) NSString *status;
/** Test to see if @c status has been set. */
@property(nonatomic, readwrite) BOOL hasStatus;
@property(nonatomic, readwrite) GtalkSharedStatus_ShowType show;
@property(nonatomic, readwrite) BOOL hasShow;
@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GtalkSharedStatus_StatusList*> *statusListArray;
/** The number of items in @c statusListArray without causing the array to be created. */
@property(nonatomic, readonly) NSUInteger statusListArray_Count;
@property(nonatomic, readwrite) BOOL invisible;
@property(nonatomic, readwrite) BOOL hasInvisible;
@property(nonatomic, readwrite) int32_t statusMinVersion;
@property(nonatomic, readwrite) BOOL hasStatusMinVersion;
@end
#pragma mark - GtalkSharedStatus_StatusList
typedef GPB_ENUM(GtalkSharedStatus_StatusList_FieldNumber) {
GtalkSharedStatus_StatusList_FieldNumber_Show = 7,
GtalkSharedStatus_StatusList_FieldNumber_StatusArray = 8,
};
@interface GtalkSharedStatus_StatusList : GPBMessage
@property(nonatomic, readwrite) GtalkSharedStatus_ShowType show;
@property(nonatomic, readwrite) BOOL hasShow;
@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<NSString*> *statusArray;
/** The number of items in @c statusArray without causing the array to be created. */
@property(nonatomic, readonly) NSUInteger statusArray_Count;
@end
#pragma mark - GtalkOtrQuery
typedef GPB_ENUM(GtalkOtrQuery_FieldNumber) {
GtalkOtrQuery_FieldNumber_NosaveDefault = 1,
GtalkOtrQuery_FieldNumber_ItemArray = 2,
GtalkOtrQuery_FieldNumber_Etag = 3,
GtalkOtrQuery_FieldNumber_NotModified = 4,
};
@interface GtalkOtrQuery : GPBMessage
@property(nonatomic, readwrite) BOOL nosaveDefault;
@property(nonatomic, readwrite) BOOL hasNosaveDefault;
@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GtalkOtrItem*> *itemArray;
/** The number of items in @c itemArray without causing the array to be created. */
@property(nonatomic, readonly) NSUInteger itemArray_Count;
@property(nonatomic, readwrite, copy, null_resettable) NSString *etag;
/** Test to see if @c etag has been set. */
@property(nonatomic, readwrite) BOOL hasEtag;
@property(nonatomic, readwrite) BOOL notModified;
@property(nonatomic, readwrite) BOOL hasNotModified;
@end
#pragma mark - GtalkOtrItem
typedef GPB_ENUM(GtalkOtrItem_FieldNumber) {
GtalkOtrItem_FieldNumber_Jid = 1,
GtalkOtrItem_FieldNumber_Nosave = 2,
GtalkOtrItem_FieldNumber_ChangedByBuddy = 3,
};
@interface GtalkOtrItem : GPBMessage
@property(nonatomic, readwrite, copy, null_resettable) NSString *jid;
/** Test to see if @c jid has been set. */
@property(nonatomic, readwrite) BOOL hasJid;
@property(nonatomic, readwrite) BOOL nosave;
@property(nonatomic, readwrite) BOOL hasNosave;
@property(nonatomic, readwrite) BOOL changedByBuddy;
@property(nonatomic, readwrite) BOOL hasChangedByBuddy;
@end
#pragma mark - GtalkIdle
typedef GPB_ENUM(GtalkIdle_FieldNumber) {
GtalkIdle_FieldNumber_Idle = 1,
GtalkIdle_FieldNumber_Away = 2,
};
@interface GtalkIdle : GPBMessage
@property(nonatomic, readwrite) BOOL idle;
@property(nonatomic, readwrite) BOOL hasIdle;
@property(nonatomic, readwrite) BOOL away;
@property(nonatomic, readwrite) BOOL hasAway;
@end
#pragma mark - GtalkPostAuthBatchQuery
typedef GPB_ENUM(GtalkPostAuthBatchQuery_FieldNumber) {
GtalkPostAuthBatchQuery_FieldNumber_Available = 1,
GtalkPostAuthBatchQuery_FieldNumber_DeviceIdle = 2,
GtalkPostAuthBatchQuery_FieldNumber_MobileIndicator = 3,
GtalkPostAuthBatchQuery_FieldNumber_SharedStatusVersion = 4,
GtalkPostAuthBatchQuery_FieldNumber_RosterEtag = 5,
GtalkPostAuthBatchQuery_FieldNumber_OtrEtag = 6,
GtalkPostAuthBatchQuery_FieldNumber_AvatarHash = 7,
GtalkPostAuthBatchQuery_FieldNumber_VcardQueryStanzaId = 8,
GtalkPostAuthBatchQuery_FieldNumber_CapabilitiesExtFlags = 9,
};
@interface GtalkPostAuthBatchQuery : GPBMessage
@property(nonatomic, readwrite) BOOL available;
@property(nonatomic, readwrite) BOOL hasAvailable;
@property(nonatomic, readwrite) BOOL deviceIdle;
@property(nonatomic, readwrite) BOOL hasDeviceIdle;
@property(nonatomic, readwrite) BOOL mobileIndicator;
@property(nonatomic, readwrite) BOOL hasMobileIndicator;
@property(nonatomic, readwrite) int32_t sharedStatusVersion;
@property(nonatomic, readwrite) BOOL hasSharedStatusVersion;
@property(nonatomic, readwrite, copy, null_resettable) NSString *rosterEtag;
/** Test to see if @c rosterEtag has been set. */
@property(nonatomic, readwrite) BOOL hasRosterEtag;
@property(nonatomic, readwrite, copy, null_resettable) NSString *otrEtag;
/** Test to see if @c otrEtag has been set. */
@property(nonatomic, readwrite) BOOL hasOtrEtag;
@property(nonatomic, readwrite, copy, null_resettable) NSString *avatarHash;
/** Test to see if @c avatarHash has been set. */
@property(nonatomic, readwrite) BOOL hasAvatarHash;
@property(nonatomic, readwrite, copy, null_resettable) NSString *vcardQueryStanzaId;
/** Test to see if @c vcardQueryStanzaId has been set. */
@property(nonatomic, readwrite) BOOL hasVcardQueryStanzaId;
@property(nonatomic, readwrite) int32_t capabilitiesExtFlags;
@property(nonatomic, readwrite) BOOL hasCapabilitiesExtFlags;
@end
#pragma mark - GtalkStreamAck
@interface GtalkStreamAck : GPBMessage
@end
#pragma mark - GtalkSelectiveAck
typedef GPB_ENUM(GtalkSelectiveAck_FieldNumber) {
GtalkSelectiveAck_FieldNumber_IdArray = 1,
};
@interface GtalkSelectiveAck : GPBMessage
@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<NSString*> *idArray;
/** The number of items in @c idArray without causing the array to be created. */
@property(nonatomic, readonly) NSUInteger idArray_Count;
@end
NS_ASSUME_NONNULL_END
CF_EXTERN_C_END
#pragma clang diagnostic pop
// @@protoc_insertion_point(global_scope)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,506 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/**
* @related FIRMessaging
*
* The completion handler invoked when the registration token returns.
* If the call fails we return the appropriate `error code`, described by
* `FIRMessagingError`.
*
* @param FCMToken The valid registration token returned by FCM.
* @param error The error describing why a token request failed. The error code
* will match a value from the FIRMessagingError enumeration.
*/
typedef void(^FIRMessagingFCMTokenFetchCompletion)(NSString * _Nullable FCMToken,
NSError * _Nullable error)
NS_SWIFT_NAME(MessagingFCMTokenFetchCompletion);
/**
* @related FIRMessaging
*
* The completion handler invoked when the registration token deletion request is
* completed. If the call fails we return the appropriate `error code`, described
* by `FIRMessagingError`.
*
* @param error The error describing why a token deletion failed. The error code
* will match a value from the FIRMessagingError enumeration.
*/
typedef void(^FIRMessagingDeleteFCMTokenCompletion)(NSError * _Nullable error)
NS_SWIFT_NAME(MessagingDeleteFCMTokenCompletion);
/**
* Callback to invoke once the HTTP call to FIRMessaging backend for updating
* subscription finishes.
*
* @param error The error which occurred while updating the subscription topic
* on the FIRMessaging server. This will be nil in case the operation
* was successful, or if the operation was cancelled.
*/
typedef void (^FIRMessagingTopicOperationCompletion)(NSError *_Nullable error);
#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
/**
* Notification sent when the upstream message has been delivered
* successfully to the server. The notification object will be the messageID
* of the successfully delivered message.
*/
FOUNDATION_EXPORT const NSNotificationName FIRMessagingSendSuccessNotification
NS_SWIFT_NAME(MessagingSendSuccess);
/**
* Notification sent when the upstream message was failed to be sent to the
* server. The notification object will be the messageID of the failed
* message. The userInfo dictionary will contain the relevant error
* information for the failure.
*/
FOUNDATION_EXPORT const NSNotificationName FIRMessagingSendErrorNotification
NS_SWIFT_NAME(MessagingSendError);
/**
* Notification sent when the Firebase messaging server deletes pending
* messages due to exceeded storage limits. This may occur, for example, when
* the device cannot be reached for an extended period of time.
*
* It is recommended to retrieve any missing messages directly from the
* server.
*/
FOUNDATION_EXPORT const NSNotificationName FIRMessagingMessagesDeletedNotification
NS_SWIFT_NAME(MessagingMessagesDeleted);
/**
* Notification sent when Firebase Messaging establishes or disconnects from
* an FCM socket connection. You can query the connection state in this
* notification by checking the `isDirectChannelEstablished` property of FIRMessaging.
*/
FOUNDATION_EXPORT const NSNotificationName FIRMessagingConnectionStateChangedNotification
NS_SWIFT_NAME(MessagingConnectionStateChanged);
/**
* Notification sent when the FCM registration token has been refreshed. Please use the
* FIRMessaging delegate method `messaging:didReceiveRegistrationToken:` to receive current and
* updated tokens.
*/
FOUNDATION_EXPORT const NSNotificationName
FIRMessagingRegistrationTokenRefreshedNotification
NS_SWIFT_NAME(MessagingRegistrationTokenRefreshed);
#else
/**
* Notification sent when the upstream message has been delivered
* successfully to the server. The notification object will be the messageID
* of the successfully delivered message.
*/
FOUNDATION_EXPORT NSString *const FIRMessagingSendSuccessNotification
NS_SWIFT_NAME(MessagingSendSuccessNotification);
/**
* Notification sent when the upstream message was failed to be sent to the
* server. The notification object will be the messageID of the failed
* message. The userInfo dictionary will contain the relevant error
* information for the failure.
*/
FOUNDATION_EXPORT NSString *const FIRMessagingSendErrorNotification
NS_SWIFT_NAME(MessagingSendErrorNotification);
/**
* Notification sent when the Firebase messaging server deletes pending
* messages due to exceeded storage limits. This may occur, for example, when
* the device cannot be reached for an extended period of time.
*
* It is recommended to retrieve any missing messages directly from the
* server.
*/
FOUNDATION_EXPORT NSString *const FIRMessagingMessagesDeletedNotification
NS_SWIFT_NAME(MessagingMessagesDeletedNotification);
/**
* Notification sent when Firebase Messaging establishes or disconnects from
* an FCM socket connection. You can query the connection state in this
* notification by checking the `isDirectChannelEstablished` property of FIRMessaging.
*/
FOUNDATION_EXPORT NSString *const FIRMessagingConnectionStateChangedNotification
NS_SWIFT_NAME(MessagingConnectionStateChangedNotification);
/**
* Notification sent when the FCM registration token has been refreshed. Please use the
* FIRMessaging delegate method `messaging:didReceiveRegistrationToken:` to receive current and
* updated tokens.
*/
FOUNDATION_EXPORT NSString *const FIRMessagingRegistrationTokenRefreshedNotification
NS_SWIFT_NAME(MessagingRegistrationTokenRefreshedNotification);
#endif // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
/**
* @enum FIRMessagingError
*/
typedef NS_ENUM(NSUInteger, FIRMessagingError) {
/// Unknown error.
FIRMessagingErrorUnknown = 0,
/// FIRMessaging couldn't validate request from this client.
FIRMessagingErrorAuthentication = 1,
/// InstanceID service cannot be accessed.
FIRMessagingErrorNoAccess = 2,
/// Request to InstanceID backend timed out.
FIRMessagingErrorTimeout = 3,
/// No network available to reach the servers.
FIRMessagingErrorNetwork = 4,
/// Another similar operation in progress, bailing this one.
FIRMessagingErrorOperationInProgress = 5,
/// Some parameters of the request were invalid.
FIRMessagingErrorInvalidRequest = 7,
/// Topic name is invalid for subscription/unsubscription.
FIRMessagingErrorInvalidTopicName = 8,
} NS_SWIFT_NAME(MessagingError);
/// Status for the downstream message received by the app.
typedef NS_ENUM(NSInteger, FIRMessagingMessageStatus) {
/// Unknown status.
FIRMessagingMessageStatusUnknown,
/// New downstream message received by the app.
FIRMessagingMessageStatusNew,
} NS_SWIFT_NAME(MessagingMessageStatus);
/**
* The APNS token type for the app. If the token type is set to `UNKNOWN`
* Firebase Messaging will implicitly try to figure out what the actual token type
* is from the provisioning profile.
* Unless you really need to specify the type, you should use the `APNSToken`
* property instead.
*/
typedef NS_ENUM(NSInteger, FIRMessagingAPNSTokenType) {
/// Unknown token type.
FIRMessagingAPNSTokenTypeUnknown,
/// Sandbox token type.
FIRMessagingAPNSTokenTypeSandbox,
/// Production token type.
FIRMessagingAPNSTokenTypeProd,
} NS_SWIFT_NAME(MessagingAPNSTokenType);
/// Information about a downstream message received by the app.
NS_SWIFT_NAME(MessagingMessageInfo)
@interface FIRMessagingMessageInfo : NSObject
/// The status of the downstream message
@property(nonatomic, readonly, assign) FIRMessagingMessageStatus status;
@end
/**
* A remote data message received by the app via FCM (not just the APNs interface).
*
* This is only for devices running iOS 10 or above. To support devices running iOS 9 or below, use
* the local and remote notifications handlers defined in UIApplicationDelegate protocol.
*/
NS_SWIFT_NAME(MessagingRemoteMessage)
@interface FIRMessagingRemoteMessage : NSObject
/// The message ID of downstream message.
@property(nonatomic, readonly, copy) NSString *messageID;
/// The downstream message received by the application.
@property(nonatomic, readonly, strong) NSDictionary *appData;
@end
@class FIRMessaging;
@class FIRMessagingExtensionHelper;
/**
* A protocol to handle token update or data message delivery from FCM.
*
*/
NS_SWIFT_NAME(MessagingDelegate)
@protocol FIRMessagingDelegate <NSObject>
@optional
/// This method will be called once a token is available, or has been refreshed. Typically it
/// will be called once per app start, but may be called more often, if token is invalidated or
/// updated. In this method, you should perform operations such as:
///
/// * Uploading the FCM token to your application server, so targeted notifications can be sent.
///
/// * Subscribing to any topics.
- (void)messaging:(FIRMessaging *)messaging
didReceiveRegistrationToken:(NSString *)fcmToken
NS_SWIFT_NAME(messaging(_:didReceiveRegistrationToken:));
/// Handle data messages received via FCM direct channel (not via APNS).
- (void)messaging:(FIRMessaging *)messaging
didReceiveMessage:(FIRMessagingRemoteMessage *)remoteMessage
NS_SWIFT_NAME(messaging(_:didReceive:));
@end
/**
* Firebase Messaging lets you reliably deliver messages at no cost.
*
* To send or receive messages, the app must get a
* registration token from FIRInstanceID. This token authorizes an
* app server to send messages to an app instance.
*
* In order to receive FIRMessaging messages, declare
* `application:didReceiveRemoteNotification::fetchCompletionHandler:`.
*/
NS_SWIFT_NAME(Messaging)
@interface FIRMessaging : NSObject
/**
* Delegate to handle FCM token refreshes, and remote data messages received via FCM direct channel.
*/
@property(nonatomic, weak, nullable) id<FIRMessagingDelegate> delegate;
/**
* When set to `YES`, Firebase Messaging will automatically establish a socket-based, direct
* channel to the FCM server. Enable this only if you are sending upstream messages or
* receiving non-APNS, data-only messages in foregrounded apps.
* Default is `NO`.
*/
@property(nonatomic) BOOL shouldEstablishDirectChannel;
/**
* Returns `YES` if the direct channel to the FCM server is active, and `NO` otherwise.
*/
@property(nonatomic, readonly) BOOL isDirectChannelEstablished;
/**
* FIRMessaging
*
* @return An instance of FIRMessaging.
*/
+ (instancetype)messaging NS_SWIFT_NAME(messaging());
/**
* FIRMessagingExtensionHelper
*
* Use FIRMessagingExtensionHelper to populate rich UI contents for your notifications.
* e.g. If an image URL is set in your notification payload or on the console, call
* FIRMessagingExtensionHelper API to render it on your notification.
*
* @return An instance of FIRMessagingExtensionHelper that handles the extensions API.
*/
+ (FIRMessagingExtensionHelper *)extensionHelper NS_SWIFT_NAME(serviceExtension())
NS_AVAILABLE(10.14, 10.0);
/**
* Unavailable. Use +messaging instead.
*/
- (instancetype)init __attribute__((unavailable("Use +messaging instead.")));
#pragma mark - APNS
/**
* This property is used to set the APNS Token received by the application delegate.
*
* FIRMessaging uses method swizzling to ensure that the APNS token is set
* automatically. However, if you have disabled swizzling by setting
* `FirebaseAppDelegateProxyEnabled` to `NO` in your app's
* Info.plist, you should manually set the APNS token in your application
* delegate's `-application:didRegisterForRemoteNotificationsWithDeviceToken:`
* method.
*
* If you would like to set the type of the APNS token, rather than relying on
* automatic detection, see: `-setAPNSToken:type:`.
*/
@property(nonatomic, copy, nullable) NSData *APNSToken NS_SWIFT_NAME(apnsToken);
/**
* Set APNS token for the application. This APNS token will be used to register
* with Firebase Messaging using `FCMToken` or
* `tokenWithAuthorizedEntity:scope:options:handler`.
*
* @param apnsToken The APNS token for the application.
* @param type The type of APNS token. Debug builds should use
* FIRMessagingAPNSTokenTypeSandbox. Alternatively, you can supply
* FIRMessagingAPNSTokenTypeUnknown to have the type automatically
* detected based on your provisioning profile.
*/
- (void)setAPNSToken:(NSData *)apnsToken type:(FIRMessagingAPNSTokenType)type;
#pragma mark - FCM Tokens
/**
* Is Firebase Messaging token auto generation enabled? If this flag is disabled,
* Firebase Messaging will not generate token automatically for message delivery.
*
* If this flag is disabled, Firebase Messaging does not generate new tokens automatically for
* message delivery. If this flag is enabled, FCM generates a registration token on application
* start when there is no existing valid token. FCM also generates a new token when an existing
* token is deleted.
*
* This setting is persisted, and is applied on future
* invocations of your application. Once explicitly set, it overrides any
* settings in your Info.plist.
*
* By default, FCM automatic initialization is enabled. If you need to change the
* default (for example, because you want to prompt the user before getting token)
* set FirebaseMessagingAutoInitEnabled to false in your application's Info.plist.
*/
@property(nonatomic, assign, getter=isAutoInitEnabled) BOOL autoInitEnabled;
/**
* The FCM token is used to identify this device so that FCM can send notifications to it.
* It is associated with your APNS token when the APNS token is supplied, so that sending
* messages to the FCM token will be delivered over APNS.
*
* The FCM token is sometimes refreshed automatically. In your FIRMessaging delegate, the
* delegate method `messaging:didReceiveRegistrationToken:` will be called once a token is
* available, or has been refreshed. Typically it should be called once per app start, but
* may be called more often, if token is invalidated or updated.
*
* Once you have an FCM token, you should send it to your application server, so it can use
* the FCM token to send notifications to your device.
*/
@property(nonatomic, readonly, nullable) NSString *FCMToken NS_SWIFT_NAME(fcmToken);
/**
* Retrieves an FCM registration token for a particular Sender ID. This can be used to allow
* multiple senders to send notifications to the same device. By providing a different Sender
* ID than your default when fetching a token, you can create a new FCM token which you can
* give to a different sender. Both tokens will deliver notifications to your device, and you
* can revoke a token when you need to.
*
* This registration token is not cached by FIRMessaging. FIRMessaging should have an APNS
* token set before calling this to ensure that notifications can be delivered via APNS using
* this FCM token. You may re-retrieve the FCM token once you have the APNS token set, to
* associate it with the FCM token. The default FCM token is automatically associated with
* the APNS token, if the APNS token data is available.
*
* @param senderID The Sender ID for a particular Firebase project.
* @param completion The completion handler to handle the token request.
*/
- (void)retrieveFCMTokenForSenderID:(NSString *)senderID
completion:(FIRMessagingFCMTokenFetchCompletion)completion
NS_SWIFT_NAME(retrieveFCMToken(forSenderID:completion:));
/**
* Invalidates an FCM token for a particular Sender ID. That Sender ID cannot no longer send
* notifications to that FCM token.
*
* @param senderID The senderID for a particular Firebase project.
* @param completion The completion handler to handle the token deletion.
*/
- (void)deleteFCMTokenForSenderID:(NSString *)senderID
completion:(FIRMessagingDeleteFCMTokenCompletion)completion
NS_SWIFT_NAME(deleteFCMToken(forSenderID:completion:));
#pragma mark - Topics
/**
* Asynchronously subscribes to a topic. This uses a FCM Token to identify
* the app instance and periodically sends data to the Firebase backend. To stop this, see
* `[FIRInstanceID deleteIDWithHandler:]`.
*
* @param topic The name of the topic, for example, @"sports".
*/
- (void)subscribeToTopic:(NSString *)topic NS_SWIFT_NAME(subscribe(toTopic:));
/**
* Asynchronously subscribe to the provided topic, retrying on failure. This uses a FCM Token
* to identify the app instance and periodically sends data to the Firebase backend. To stop this,
* see `[FIRInstanceID deleteIDWithHandler:]`.
*
* @param topic The topic name to subscribe to, for example, @"sports".
* @param completion The completion that is invoked once the subscribe call ends.
* In case of success, nil error is returned. Otherwise, an
* appropriate error object is returned.
*/
- (void)subscribeToTopic:(nonnull NSString *)topic
completion:(nullable FIRMessagingTopicOperationCompletion)completion;
/**
* Asynchronously unsubscribe from a topic. This uses a FCM Token
* to identify the app instance and periodically sends data to the Firebase backend. To stop this,
* see `[FIRInstanceID deleteIDWithHandler:]`.
*
* @param topic The name of the topic, for example @"sports".
*/
- (void)unsubscribeFromTopic:(NSString *)topic NS_SWIFT_NAME(unsubscribe(fromTopic:));
/**
* Asynchronously unsubscribe from the provided topic, retrying on failure. This uses a FCM Token
* to identify the app instance and periodically sends data to the Firebase backend. To stop this,
* see `[FIRInstanceID deleteIDWithHandler:]`.
*
* @param topic The topic name to unsubscribe from, for example @"sports".
* @param completion The completion that is invoked once the unsubscribe call ends.
* In case of success, nil error is returned. Otherwise, an
* appropriate error object is returned.
*/
- (void)unsubscribeFromTopic:(nonnull NSString *)topic
completion:(nullable FIRMessagingTopicOperationCompletion)completion;
#pragma mark - Upstream
/**
* Sends an upstream ("device to cloud") message.
*
* The message is queued if we don't have an active connection.
* You can only use the upstream feature if your FCM implementation
* uses the XMPP server protocol.
*
* @param message Key/Value pairs to be sent. Values must be String, any
* other type will be ignored.
* @param receiver A string identifying the receiver of the message. For FCM
* project IDs the value is `SENDER_ID@gcm.googleapis.com`.
* @param messageID The ID of the message. This is generated by the application. It
* must be unique for each message generated by this application.
* It allows error callbacks and debugging, to uniquely identify
* each message.
* @param ttl The time to live for the message. In case we aren't able to
* send the message before the TTL expires we will send you a
* callback. If 0, we'll attempt to send immediately and return
* an error if we're not connected. Otherwise, the message will
* be queued. As for server-side messages, we don't return an error
* if the message has been dropped because of TTL; this can happen
* on the server side, and it would require extra communication.
*/
- (void)sendMessage:(NSDictionary *)message
to:(NSString *)receiver
withMessageID:(NSString *)messageID
timeToLive:(int64_t)ttl;
#pragma mark - Analytics
/**
* Use this to track message delivery and analytics for messages, typically
* when you receive a notification in `application:didReceiveRemoteNotification:`.
* However, you only need to call this if you set the `FirebaseAppDelegateProxyEnabled`
* flag to `NO` in your Info.plist. If `FirebaseAppDelegateProxyEnabled` is either missing
* or set to `YES` in your Info.plist, the library will call this automatically.
*
* @param message The downstream message received by the application.
*
* @return Information about the downstream message.
*/
- (FIRMessagingMessageInfo *)appDidReceiveMessage:(NSDictionary *)message;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,36 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* 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
#import <UserNotifications/UserNotifications.h>
#endif
NS_ASSUME_NONNULL_BEGIN
__IOS_AVAILABLE(10.0) __OSX_AVAILABLE(10.14)
@interface FIRMessagingExtensionHelper : NSObject
/// Call this API to complete your notification content modification. If you like to
/// overwrite some properties of the content instead of using the default payload,
/// make sure to make your customized motification to the content before passing it to
/// this call.
- (void)populateNotificationContent:(UNMutableNotificationContent *)content
withContentHandler:(void (^)(UNNotificationContent *_Nonnull))contentHandler;
@end
NS_ASSUME_NONNULL_END
+18
View File
@@ -0,0 +1,18 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessaging.h"
#import "FIRMessagingExtensionHelper.h"