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"
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.
+223
View File
@@ -0,0 +1,223 @@
# Firebase iOS Open Source Development [![Build Status](https://travis-ci.org/firebase/firebase-ios-sdk.svg?branch=master)](https://travis-ci.org/firebase/firebase-ios-sdk)
This repository contains a subset of the Firebase iOS SDK source. It currently
includes FirebaseCore, FirebaseAuth, FirebaseDatabase, FirebaseFirestore,
FirebaseFunctions, FirebaseInstanceID, FirebaseInAppMessaging,
FirebaseInAppMessagingDisplay, FirebaseMessaging and FirebaseStorage.
The repository also includes GoogleUtilities source. The
[GoogleUtilities](GoogleUtilities/README.md) pod is
a set of utilities used by Firebase and other Google products.
Firebase is an app development platform with tools to help you build, grow and
monetize your app. More information about Firebase can be found at
[https://firebase.google.com](https://firebase.google.com).
## Installation
See the three subsections for details about three different installation methods.
1. [Standard pod install](README.md#standard-pod-install)
1. [Installing from the GitHub repo](README.md#installing-from-github)
1. [Experimental Carthage](README.md#carthage-ios-only)
### Standard pod install
Go to
[https://firebase.google.com/docs/ios/setup](https://firebase.google.com/docs/ios/setup).
### Installing from GitHub
For releases starting with 5.0.0, the source for each release is also deployed
to CocoaPods master and available via standard
[CocoaPods Podfile syntax](https://guides.cocoapods.org/syntax/podfile.html#pod).
These instructions can be used to access the Firebase repo at other branches,
tags, or commits.
#### Background
See
[the Podfile Syntax Reference](https://guides.cocoapods.org/syntax/podfile.html#pod)
for instructions and options about overriding pod source locations.
#### Accessing Firebase Source Snapshots
All of the official releases are tagged in this repo and available via CocoaPods. To access a local
source snapshot or unreleased branch, use Podfile directives like the following:
To access FirebaseFirestore via a branch:
```
pod 'FirebaseCore', :git => 'https://github.com/firebase/firebase-ios-sdk.git', :branch => 'master'
pod 'FirebaseFirestore', :git => 'https://github.com/firebase/firebase-ios-sdk.git', :branch => 'master'
```
To access FirebaseMessaging via a checked out version of the firebase-ios-sdk repo do:
```
pod 'FirebaseCore', :path => '/path/to/firebase-ios-sdk'
pod 'FirebaseMessaging', :path => '/path/to/firebase-ios-sdk'
```
### Carthage (iOS only)
Instructions for the experimental Carthage distribution are at
[Carthage](Carthage.md).
### Rome
Instructions for installing binary frameworks via
[Rome](https://github.com/CocoaPods/Rome) are at [Rome](Rome.md).
## Development
To develop Firebase software in this repository, ensure that you have at least
the following software:
* Xcode 10.1 (or later)
* CocoaPods 1.7.2 (or later)
For the pod that you want to develop:
`pod gen Firebase{name here}.podspec --local-sources=./ --auto-open`
Firestore and Functions have self contained Xcode projects. See
[Firestore/README.md](Firestore/README.md) and
[Functions/README.md](Functions/README.md).
### Adding a New Firebase Pod
See [AddNewPod.md](AddNewPod.md).
### Code Formatting
To ensure that the code is formatted consistently, run the script
[./scripts/style.sh](https://github.com/firebase/firebase-ios-sdk/blob/master/scripts/style.sh)
before creating a PR.
Travis will verify that any code changes are done in a style compliant way. Install
`clang-format` and `swiftformat`.
These commands will get the right versions:
```
brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/773cb75d360b58f32048f5964038d09825a507c8/Formula/clang-format.rb
brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/3dfea1004e0736754bbf49673cca8aaed8a94089/Formula/swiftformat.rb
```
Note: if you already have a newer version of these installed you may need to
`brew switch` to this version.
### Running Unit Tests
Select a scheme and press Command-u to build a component and run its unit tests.
#### Viewing Code Coverage
First, make sure that [xcov](https://github.com/nakiostudio/xcov) is installed with `gem install xcov`.
After running the `AllUnitTests_iOS` scheme in Xcode, execute
`xcov --workspace Firebase.xcworkspace --scheme AllUnitTests_iOS --output_directory xcov_output`
at Example/ in the terminal. This will aggregate the coverage, and you can run `open xcov_output/index.html` to see the results.
### Running Sample Apps
In order to run the sample apps and integration tests, you'll need valid
`GoogleService-Info.plist` files for those samples. The Firebase Xcode project contains dummy plist
files without real values, but can be replaced with real plist files. To get your own
`GoogleService-Info.plist` files:
1. Go to the [Firebase Console](https://console.firebase.google.com/)
2. Create a new Firebase project, if you don't already have one
3. For each sample app you want to test, create a new Firebase app with the sample app's bundle
identifier (e.g. `com.google.Database-Example`)
4. Download the resulting `GoogleService-Info.plist` and replace the appropriate dummy plist file
(e.g. in [Example/Database/App/](Example/Database/App/));
Some sample apps like Firebase Messaging ([Example/Messaging/App](Example/Messaging/App)) require
special Apple capabilities, and you will have to change the sample app to use a unique bundle
identifier that you can control in your own Apple Developer account.
## Specific Component Instructions
See the sections below for any special instructions for those components.
### Firebase Auth
If you're doing specific Firebase Auth development, see
[the Auth Sample README](Example/Auth/README.md) for instructions about
building and running the FirebaseAuth pod along with various samples and tests.
### Firebase Database
To run the Database Integration tests, make your database authentication rules
[public](https://firebase.google.com/docs/database/security/quickstart).
### Firebase Storage
To run the Storage Integration tests, follow the instructions in
[FIRStorageIntegrationTests.m](Example/Storage/Tests/Integration/FIRStorageIntegrationTests.m).
#### Push Notifications
Push notifications can only be delivered to specially provisioned App IDs in the developer portal.
In order to actually test receiving push notifications, you will need to:
1. Change the bundle identifier of the sample app to something you own in your Apple Developer
account, and enable that App ID for push notifications.
2. You'll also need to
[upload your APNs Provider Authentication Key or certificate to the Firebase Console](https://firebase.google.com/docs/cloud-messaging/ios/certs)
at **Project Settings > Cloud Messaging > [Your Firebase App]**.
3. Ensure your iOS device is added to your Apple Developer portal as a test device.
#### iOS Simulator
The iOS Simulator cannot register for remote notifications, and will not receive push notifications.
In order to receive push notifications, you'll have to follow the steps above and run the app on a
physical device.
## Community Supported Efforts
We've seen an amazing amount of interest and contributions to improve the Firebase SDKs, and we are
very grateful! We'd like to empower as many developers as we can to be able to use Firebase and
participate in the Firebase community.
### macOS and tvOS
Thanks to contributions from the community, FirebaseAuth, FirebaseCore, FirebaseDatabase, FirebaseMessaging,
FirebaseFirestore, FirebaseFunctions and FirebaseStorage now compile, run unit tests, and work on
macOS and tvOS.
For tvOS, checkout the [Sample](Example/tvOSSample).
Keep in mind that macOS and tvOS are not officially supported by Firebase, and this repository is
actively developed primarily for iOS. While we can catch basic unit test issues with Travis, there
may be some changes where the SDK no longer works as expected on macOS or tvOS. If you encounter
this, please [file an issue](https://github.com/firebase/firebase-ios-sdk/issues).
Note that the Firebase pod is not available for macOS and tvOS.
To install, add a subset of the following to the Podfile:
```
pod 'FirebaseAuth'
pod 'FirebaseCore'
pod 'FirebaseDatabase'
pod 'FirebaseFirestore'
pod 'FirebaseFunctions'
pod 'FirebaseMessaging'
pod 'FirebaseStorage'
```
## Roadmap
See [Roadmap](ROADMAP.md) for more about the Firebase iOS SDK Open Source
plans and directions.
## Contributing
See [Contributing](CONTRIBUTING.md) for more information on contributing to the Firebase
iOS SDK.
## License
The contents of this repository is licensed under the
[Apache License, version 2.0](http://www.apache.org/licenses/LICENSE-2.0).
Your use of Firebase is governed by the
[Terms of Service for Firebase Services](https://firebase.google.com/terms/).