aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firebase/Messaging/FIRMessagingDataMessageManager.m
diff options
context:
space:
mode:
authorGravatar Paul Beusterien <paulbeusterien@google.com>2017-05-15 12:27:07 -0700
committerGravatar Paul Beusterien <paulbeusterien@google.com>2017-05-15 12:27:07 -0700
commit98ba64449a632518bd2b86fe8d927f4a960d3ddc (patch)
tree131d9c4272fa6179fcda6c5a33fcb3b1bd57ad2e /Firebase/Messaging/FIRMessagingDataMessageManager.m
parent32461366c9e204a527ca05e6e9b9404a2454ac51 (diff)
Initial
Diffstat (limited to 'Firebase/Messaging/FIRMessagingDataMessageManager.m')
-rw-r--r--Firebase/Messaging/FIRMessagingDataMessageManager.m545
1 files changed, 545 insertions, 0 deletions
diff --git a/Firebase/Messaging/FIRMessagingDataMessageManager.m b/Firebase/Messaging/FIRMessagingDataMessageManager.m
new file mode 100644
index 0000000..2433bd4
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingDataMessageManager.m
@@ -0,0 +1,545 @@
+/*
+ * 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"
+
+// The Notification used to send InstanceID messages that FIRMessaging receives.
+static NSString *const NOTIFICATION_IID_MESSAGE = @"com.google.gcm/notification/iid";
+
+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 kFromForInstanceIDMessages = @"google.com/iid";
+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];
+ }];
+}
+
+- (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;
+ } else if ([kFromForInstanceIDMessages isEqualToString:from]) {
+ // send message to InstanceID library.
+ NSMutableDictionary *message = [NSMutableDictionary dictionary];
+ for (GtalkAppData *item in dataMessage.appDataArray) {
+ _FIRMessagingDevAssert(item.key && item.value, @"Invalid app data item");
+ if (item.key && item.value) {
+ message[item.key] = item.value;
+ }
+ }
+
+ [[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_IID_MESSAGE
+ object:message];
+ 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