Archived
Push from command line
This commit is contained in:
@@ -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,
|
||||
|
||||
};
|
||||
+1221
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
|
||||
+44
@@ -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
|
||||
+202
@@ -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
|
||||
+146
@@ -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
|
||||
+28
@@ -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
|
||||
+54
@@ -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—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
|
||||
+45
@@ -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
|
||||
+602
@@ -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
|
||||
+201
@@ -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
|
||||
+817
@@ -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"
|
||||
+30
@@ -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
|
||||
+36
@@ -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
@@ -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"
|
||||
Generated
+202
@@ -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.
|
||||
Generated
+223
@@ -0,0 +1,223 @@
|
||||
# Firebase iOS Open Source Development [](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/).
|
||||
Reference in New Issue
Block a user