aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firebase/Messaging/FIRMessagingClient.m
diff options
context:
space:
mode:
Diffstat (limited to 'Firebase/Messaging/FIRMessagingClient.m')
-rw-r--r--Firebase/Messaging/FIRMessagingClient.m490
1 files changed, 490 insertions, 0 deletions
diff --git a/Firebase/Messaging/FIRMessagingClient.m b/Firebase/Messaging/FIRMessagingClient.m
new file mode 100644
index 0000000..c01aecc
--- /dev/null
+++ b/Firebase/Messaging/FIRMessagingClient.m
@@ -0,0 +1,490 @@
+/*
+ * 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 "FIRMessagingConnection.h"
+#import "FIRMessagingDataMessageManager.h"
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingRegistrar.h"
+#import "FIRMessagingRmqManager.h"
+#import "FIRMessagingTopicsCommon.h"
+#import "FIRMessagingUtilities.h"
+#import "FIRReachabilityChecker.h"
+#import "NSError+FIRMessaging.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, readwrite, strong) FIRMessagingRegistrar *registrar;
+
+@property(nonatomic, readwrite, strong) NSString *senderId;
+
+// FIRMessagingService owns these instances
+@property(nonatomic, readwrite, weak) FIRMessagingRmqManager *rmq2Manager;
+@property(nonatomic, readwrite, weak) FIRReachabilityChecker *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:(FIRReachabilityChecker *)reachability
+ rmq2Manager:(FIRMessagingRmqManager *)rmq2Manager {
+ self = [super init];
+ if (self) {
+ _reachability = reachability;
+ _clientDelegate = delegate;
+ _rmq2Manager = rmq2Manager;
+ _registrar = [[FIRMessagingRegistrar alloc] init];
+ _connectionTimeoutInterval = kConnectTimeoutInterval;
+ }
+ 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 cancelAllRequests];
+
+ _FIRMessagingDevAssert(self.connection.state == kFIRMessagingConnectionNotConnected, @"Did not disconnect");
+ [NSObject cancelPreviousPerformRequestsWithTarget:self];
+}
+
+- (void)cancelAllRequests {
+ // Stop any checkin requests or any subscription requests
+ [self.registrar cancelAllRequests];
+
+ // 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(FIRMessagingTopicOperationResult result, 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(result, error);
+ };
+
+ [self.registrar tryToLoadValidCheckinInfo];
+ [self.registrar updateSubscriptionToTopic:topic
+ withToken:token
+ options:options
+ shouldDelete:shouldDelete
+ handler:completion];
+}
+
+#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.registrar tryToLoadValidCheckinInfo];
+ [self connect];
+}
+
+- (void)connect {
+ // reset retry counts
+ self.connectRetryCount = 0;
+
+ if (self.isConnected) {
+ return;
+ }
+
+ self.stayConnected = YES;
+ BOOL isRegistrationComplete = [self.registrar hasValidCheckinInfo];
+
+ if (!isRegistrationComplete) {
+ if (![self.registrar tryToLoadValidCheckinInfo]) {
+ if (self.connectHandler) {
+ NSError *error = [NSError errorWithFCMErrorCode:kFIRMessagingErrorCodeMissingDeviceID];
+ self.connectHandler(error);
+ }
+ FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient009,
+ @"Failed to connect to MCS. No deviceID and secret found.");
+ 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 - 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:self.registrar.deviceAuthID
+ secretToken:self.registrar.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:self.registrar.deviceAuthID
+ token:self.registrar.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(self.registrar.deviceAuthID.length > 0 &&
+ self.registrar.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 {
+ FIRReachabilityStatus status = self.reachability.reachabilityStatus;
+ BOOL isReachable = (status == kFIRReachabilityViaWifi || status == kFIRReachabilityViaCellular);
+ 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